commit a5f98b02d48fc92c6e870e832aec25203f6ba305 Author: James Seibel Date: Wed Nov 10 22:02:38 2021 -0600 Initial Commit (from Lod Mod 1.16.5 11-10-2021) diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..f811f6ae6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Disable autocrlf on generated files, they always generate with LF +# Add any extra files or paths here to make git stop saying they +# are changed when only line endings change. +src/generated/**/.cache/cache text eol=lf +src/generated/**/*.json text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..a4fbb01a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# eclipse +bin +*.launch +.settings +.metadata +.classpath +.project + +# idea +out +*.ipr +*.iws +*.iml +.idea + +# gradle +build +.gradle + +# other +eclipse +run + +# Files from Forge MDK +logs +forge*changelog.txt diff --git a/1.5 release notes.txt b/1.5 release notes.txt new file mode 100644 index 000000000..945346b38 Binary files /dev/null and b/1.5 release notes.txt differ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..810fce6e9 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,621 @@ + 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 diff --git a/Readme.md b/Readme.md new file mode 100644 index 000000000..ec12d59d6 --- /dev/null +++ b/Readme.md @@ -0,0 +1,68 @@ +# Distant Horizons + +This mod adds a Level Of Detail (LOD) system to Minecraft.\ +This implementation renders simplified chunks outside the normal render distance\ +allowing for an increased view distance without harming performance. + +Or in other words: this mod lets you see farther without turning your game into a slide show.\ +If you want to see a quick demo, check out a video covering the mod here: + +![Minecraft Level Of Detail (LOD) mod - Alpha 1.4](https://i.ytimg.com/vi_webp/H2tnvEVbO1c/mqdefault.webp) + + +Forge version: 1.16.5-36.1.0 + +Notes:\ +This version has been confirmed to work in Eclipse and Retail Minecraft.\ +(Retail running forge version 1.16.5-36.1.0) + + +## source code installation + +See the Forge Documentation online for more detailed instructions:\ +http://mcforge.readthedocs.io/en/latest/gettingstarted/ + +1. Create a system variable called "JAVA_MC_HOME" with the location of the JDK 1.8.0_251 (This is needed for gradle to work correctly) +2. replace JAVA_HOME with JAVA_MC_HOME in gradle.bat +3. open a command line in the project folder + +**If using Ecplise:** +1. run the command: `./gradlew geneclipseruns` +2. run the command: `./gradlew eclipse` +3. Make sure eclipse has the JDK 1.8.0_251 installed. (This is needed so that eclipse can run minecraft) +4. Import the project into eclipse + +**If using IntelliJ:** +1. open IDEA and import the build.gradle +2. run the command: `./gradlew genIntellijRuns` +3. refresh the Gradle project in IDEA if required + + +## Compiling + +1. open a command line in the project folder +2. run the command: `./gradlew build` +3. the compiled jar file will be in the folder `build\libs` + + +## Other commands + +`./gradlew --refresh-dependencies` to refresh local dependencies. + +`./gradlew clean` to reset everything (this does not affect your code) and then start the process again. + + +## Note to self + +The Minecraft source code is NOT added to your workspace in an editable way. Minecraft is treated like a normal Library. Sources are there for documentation and research purposes only. + +Source code uses Mojang mappings. + +The source code can be 'created' with the `./eclipse` command and can be found in the following path:\ +`minecraft-lod-mod\build\fg_cache\mcp\ VERSION \joined\ RANDOM_STRING \patch\output.jar` + + +## Open Source Acknowledgements + +XZ for Java (data compression)\ +https://tukaani.org/xz/java.html diff --git a/_IDE files/Eclipse Auto Formatting V1.xml b/_IDE files/Eclipse Auto Formatting V1.xml new file mode 100644 index 000000000..659ba3b52 --- /dev/null +++ b/_IDE files/Eclipse Auto Formatting V1.xml @@ -0,0 +1,390 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_IDE files/intellij Auto Formatting V1.xml b/_IDE files/intellij Auto Formatting V1.xml new file mode 100644 index 000000000..126cc559c --- /dev/null +++ b/_IDE files/intellij Auto Formatting V1.xml @@ -0,0 +1,50 @@ + + \ No newline at end of file diff --git a/_logo files/LOD logo flat - with boarder.png b/_logo files/LOD logo flat - with boarder.png new file mode 100644 index 000000000..35f7242cf Binary files /dev/null and b/_logo files/LOD logo flat - with boarder.png differ diff --git a/_logo files/LOD logo flat.png b/_logo files/LOD logo flat.png new file mode 100644 index 000000000..cf94381fc Binary files /dev/null and b/_logo files/LOD logo flat.png differ diff --git a/_logo files/LOD logo shade.png b/_logo files/LOD logo shade.png new file mode 100644 index 000000000..f2ff21017 Binary files /dev/null and b/_logo files/LOD logo shade.png differ diff --git a/_logo files/LogoNameFlat.png b/_logo files/LogoNameFlat.png new file mode 100644 index 000000000..ba695ce06 Binary files /dev/null and b/_logo files/LogoNameFlat.png differ diff --git a/_logo files/LogoNameFlatAlt.png b/_logo files/LogoNameFlatAlt.png new file mode 100644 index 000000000..0caf9c6d1 Binary files /dev/null and b/_logo files/LogoNameFlatAlt.png differ diff --git a/_logo files/LogoNameShade.png b/_logo files/LogoNameShade.png new file mode 100644 index 000000000..f709d51be Binary files /dev/null and b/_logo files/LogoNameShade.png differ diff --git a/_logo files/LogoNameShadeAlt.png b/_logo files/LogoNameShadeAlt.png new file mode 100644 index 000000000..d42d720dd Binary files /dev/null and b/_logo files/LogoNameShadeAlt.png differ diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..6c60dd699 --- /dev/null +++ b/build.gradle @@ -0,0 +1,234 @@ +buildscript { + repositories { + maven { url = 'https://files.minecraftforge.net' } + mavenCentral() + // potential replacement in case of problems: + // https://dist.creeper.host/Sponge/maven + maven { url = 'https://repo.spongepowered.org/maven/' } + // used to download and compile dependencies from git repos + maven { url 'https://jitpack.io' } + } + dependencies { + classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '3.+', changing: true + classpath group: 'org.spongepowered', name: 'mixingradle', version: '0.7-SNAPSHOT' + classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.2' + } +} +apply plugin: 'net.minecraftforge.gradle' +apply plugin: 'org.spongepowered.mixin' +// Only edit below this line, the above code adds and enables the necessary things for Forge to be setup. +apply plugin: 'eclipse' +apply plugin: 'maven-publish' +apply plugin: 'com.github.johnrengelman.shadow' + +version = 'a1.5.2' +group = 'com.seibel.lod' +archivesBaseName = 'Distant-Horizons_1.16.5' + + +sourceCompatibility = targetCompatibility = compileJava.sourceCompatibility = compileJava.targetCompatibility = '1.8' // Need this here so eclipse task generates correctly. + +println('Java: ' + System.getProperty('java.version') + ' JVM: ' + System.getProperty('java.vm.version') + '(' + System.getProperty('java.vendor') + ') Arch: ' + System.getProperty('os.arch')) +minecraft { + // The mappings can be changed at any time, and must be in the following format. + // snapshot_YYYYMMDD Snapshot are built nightly. + // stable_# Stables are built at the discretion of the MCP team. + // Use non-default mappings at your own risk. they may not always work. + // Simply re-run your setup task after changing the mappings to update your workspace. + mappings channel: 'official', version: '1.16.5' + + // makeObfSourceJar = false // an Srg named sources jar is made by default. uncomment this to disable. + + accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg') + + // Default run configurations. + // These can be tweaked, removed, or duplicated as needed. + runs { + client { + workingDirectory project.file('run') + arg "-mixin.config=lod.mixins.json" + + // Recommended logging data for a userdev environment + // The markers can be changed as needed. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + property 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + property 'forge.logging.console.level', 'debug' + + mods { + examplemod { + source sourceSets.main + } + } + } + + server { + workingDirectory project.file('run') + arg "-mixin.config=lod.mixins.json" + + // Recommended logging data for a userdev environment + // The markers can be changed as needed. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + property 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + property 'forge.logging.console.level', 'debug' + + mods { + examplemod { + source sourceSets.main + } + } + } + + data { + workingDirectory project.file('run') + + // Recommended logging data for a userdev environment + // The markers can be changed as needed. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + property 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + property 'forge.logging.console.level', 'debug' + + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. + args '--mod', 'lod', '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/') + + mods { + examplemod { + source sourceSets.main + } + } + } + } +} + +// Include resources generated by data generators. +sourceSets.main.resources { srcDir 'src/generated/resources' } + +// this is required so that we can use +// jitpack in the dependencies section below +repositories { + mavenCentral() + // used to download and compile dependencies from git repos + maven { url 'https://jitpack.io' } +} + +configurations { + shadowMe + compileOnly.extendsFrom(embed) +} + +dependencies { + // Specify the version of Minecraft to use, If this is any group other then 'net.minecraft' it is assumed + // that the dep is a ForgeGradle 'patcher' dependency. And it's patches will be applied. + // The userdev artifact is a special name and will get all sorts of transformations applied to it. + minecraft 'net.minecraftforge:forge:1.16.5-36.1.0' + + + compile 'org.tukaani:xz:1.9' + shadowMe 'org.tukaani:xz:1.9' + compile 'org.apache.commons:commons-compress:1.21' + shadowMe 'org.apache.commons:commons-compress:1.21' + + // these were added to hopefully allow for cloning + // configuredFeatures to allow for safe + // multi threaded feature generation. Sadly I couldn't find + // a way to duplicate lambda functions (which features use) + // so for now I'm not sure what to do. + //implementation 'io.github.kostaskougios:cloning:1.10.3' + // + //implementation ('com.esotericsoftware:kryo:5.1.1') { + // exclude group: "org.objenesis" + //} + //implementation 'org.objenesis:objenesis:3.2' + + + // You may put jars on which you depend on in ./libs or you may define them like so.. + // compile "some.group:artifact:version:classifier" + // compile "some.group:artifact:version" + + // Real examples + // compile 'com.mod-buildcraft:buildcraft:6.0.8:dev' // adds buildcraft to the dev env + // compile 'com.googlecode.efficient-java-matrix-library:ejml:0.24' // adds ejml to the dev env + + // The 'provided' configuration is for optional dependencies that exist at compile-time but might not at runtime. + // provided 'com.mod-buildcraft:buildcraft:6.0.8:dev' + + // These dependencies get remapped to your current MCP mappings + // deobf 'com.mod-buildcraft:buildcraft:6.0.8:dev' + + // For more info... + // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html + // http://www.gradle.org/docs/current/userguide/dependency_management.html + +} + +shadowJar { + duplicatesStrategy = DuplicatesStrategy.INCLUDE + configurations = [project.configurations.getByName("shadowMe")] + relocate 'org.tukaani', 'shaded.tukaani' + relocate 'org.apache.commons.compress', 'shaded.apache.commons.compress' + classifier = '' +} + +reobf { + shadowJar { + dependsOn tasks.createMcpToSrg + mappings = tasks.createMcpToSrg.outputs.files.singleFile + } +} + +artifacts { + archives tasks.shadowJar +} + +// Example for how to get properties into the manifest for reading by the runtime.. +jar { + manifest { + attributes([ + "Specification-Title": "LOD", + "Specification-Version": "1", // We are version 1 of ourselves + "Implementation-Title": project.name, + "Implementation-Version": "1", + "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"), + "MixinConfigs": "lod.mixins.json", + ]) + } +} +// Example configuration to allow publishing using the maven-publish task +// This is the preferred method to reobfuscate your jar file +jar.finalizedBy('reobfJar') +// However if you are in a multi-project build, dev time needs unobfed jar files, so you can delay the obfuscation until publishing by doing +//publish.dependsOn('reobfJar') + +publishing { + publications { + mavenJava(MavenPublication) { + artifact jar + } + } + repositories { + maven { + url "file:///${project.projectDir}/mcmodsrepo" + } + } +} + +mixin { + add sourceSets.main, "lod.refmap.json" +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..878bf1f7e --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +# Sets default memory used for gradle commands. Can be overridden by user or command line properties. +# This is required to provide enough memory for the Minecraft decompilation process. +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..7a3265ee9 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..1d5b29fbd --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-bin.zip diff --git a/gradlew b/gradlew new file mode 100644 index 000000000..cccdd3d51 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..808465d8d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_MC_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_MC_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_MC_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_MC_HOME=%JAVA_MC_HOME:"=% +set JAVA_EXE=%JAVA_MC_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_MC_HOME is set to an invalid directory: %JAVA_MC_HOME% +echo. +echo Please set the JAVA_MC_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/license_header.txt b/license_header.txt new file mode 100644 index 000000000..0661f70d8 Binary files /dev/null and b/license_header.txt differ diff --git a/src/main/java/com/seibel/lod/LodMain.java b/src/main/java/com/seibel/lod/LodMain.java new file mode 100644 index 000000000..19e88b0bc --- /dev/null +++ b/src/main/java/com/seibel/lod/LodMain.java @@ -0,0 +1,82 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod; + +import com.seibel.lod.config.LodConfig; +import com.seibel.lod.proxy.ClientProxy; + +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.config.ModConfig; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.event.server.FMLServerStartingEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; + +/** + * Initialize and setup the Mod. + *
+ * If you are looking for the real start of the mod + * check out the ClientProxy. + * + * @author James Seibel + * @version 7-3-2021 + */ +@Mod(ModInfo.ID) +public class LodMain +{ + public static LodMain instance; + + public static ClientProxy client_proxy; + + + private void init(final FMLCommonSetupEvent event) + { + ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, LodConfig.CLIENT_SPEC); + } + + + public LodMain() + { + // Register the methods + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::init); + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::onClientStart); + + // Register ourselves for server and other game events we are interested in + MinecraftForge.EVENT_BUS.register(this); + } + + private void onClientStart(final FMLClientSetupEvent event) + { + client_proxy = new ClientProxy(); + MinecraftForge.EVENT_BUS.register(client_proxy); + } + + + + @SubscribeEvent + public void onServerStarting(FMLServerStartingEvent event) + { + // this is called when the server starts + } + +} diff --git a/src/main/java/com/seibel/lod/ModInfo.java b/src/main/java/com/seibel/lod/ModInfo.java new file mode 100644 index 000000000..8418b46a8 --- /dev/null +++ b/src/main/java/com/seibel/lod/ModInfo.java @@ -0,0 +1,35 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod; + +/** + * This file is similar to mcmod.info + * @author James Seibel + * @version 10-23-2021 + */ +public final class ModInfo +{ + public static final String ID = "lod"; + public static final String NAME = "DistantHorizons"; + /** Human readable version of MOD_NAME */ + public static final String READABLE_NAME = "Distant Horizons"; + public static final String API = "LodAPI"; + public static final String VERSION = "a1.5.2"; +} \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/builders/bufferBuilding/LodBufferBuilder.java b/src/main/java/com/seibel/lod/builders/bufferBuilding/LodBufferBuilder.java new file mode 100644 index 000000000..45bc7231e --- /dev/null +++ b/src/main/java/com/seibel/lod/builders/bufferBuilding/LodBufferBuilder.java @@ -0,0 +1,975 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.builders.bufferBuilding; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.locks.ReentrantLock; + +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GL45; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.mojang.blaze3d.systems.RenderSystem; +import com.seibel.lod.builders.bufferBuilding.lodTemplates.Box; +import com.seibel.lod.config.LodConfig; +import com.seibel.lod.enums.GlProxyContext; +import com.seibel.lod.enums.GpuUploadMethod; +import com.seibel.lod.enums.VanillaOverdraw; +import com.seibel.lod.objects.LodDimension; +import com.seibel.lod.objects.LodRegion; +import com.seibel.lod.objects.PosToRenderContainer; +import com.seibel.lod.objects.RegionPos; +import com.seibel.lod.proxy.ClientProxy; +import com.seibel.lod.proxy.GlProxy; +import com.seibel.lod.render.LodRenderer; +import com.seibel.lod.util.DataPointUtil; +import com.seibel.lod.util.DetailDistanceUtil; +import com.seibel.lod.util.LevelPosUtil; +import com.seibel.lod.util.LodThreadFactory; +import com.seibel.lod.util.LodUtil; +import com.seibel.lod.util.ThreadMapUtil; +import com.seibel.lod.wrappers.MinecraftWrapper; + +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.client.renderer.vertex.VertexBuffer; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.util.Direction; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.LightType; + +/** + * This object is used to create NearFarBuffer objects. + * @author James Seibel + * @version 10-23-2021 + */ +public class LodBufferBuilder +{ + private static final MinecraftWrapper mc = MinecraftWrapper.INSTANCE; + + /** The thread used to generate new LODs off the main thread. */ + public static final ExecutorService mainGenThread = Executors.newSingleThreadExecutor(new LodThreadFactory(LodBufferBuilder.class.getSimpleName() + " - main")); + /** The threads used to generate buffers. */ + public static final ExecutorService bufferBuilderThreads = Executors.newFixedThreadPool(LodConfig.CLIENT.advancedModOptions.threading.numberOfBufferBuilderThreads.get(), new ThreadFactoryBuilder().setNameFormat("Buffer-Builder-%d").build()); + + /** + * When uploading to a buffer that is too small, + * recreate it this many times bigger than the upload payload + */ + public static final double BUFFER_EXPANSION_MULTIPLIER = 1.5; + + /** + * When buffers are first created they are allocated to this size (in Bytes). + * This size will be too small, more than likely. The buffers will be expanded + * when need be to fit the larger sizes. + */ + public static final int DEFAULT_MEMORY_ALLOCATION = 1024; + + public static int skyLightPlayer = 15; + + /** + * How many buffers there are for the given region.
+ * This is done because some regions may require more memory than + * can be directly allocated, so we split the regions into smaller sections.
+ * This keeps track of those sections. + */ + public volatile int[][] numberOfBuffersPerRegion; + + /** Stores the vertices when building the VBOs */ + public volatile BufferBuilder[][][] buildableBuffers; + + /** The OpenGL IDs of the storage buffers used by the buildableVbos */ + public int[][][] buildableStorageBufferIds; + /** The OpenGL IDs of the storage buffers used by the drawableVbos */ + public int[][][] drawableStorageBufferIds; + + /** Used when building new VBOs */ + public volatile VertexBuffer[][][] buildableVbos; + /** VBOs that are sent over to the LodNodeRenderer */ + public volatile VertexBuffer[][][] drawableVbos; + + /** + * if this is true the LOD buffers are currently being + * regenerated. + */ + public boolean generatingBuffers = false; + + /** + * if this is true new LOD buffers have been generated + * and are waiting to be swapped with the drawable buffers + */ + private boolean switchVbos = false; + + /** Size of the buffer builders in bytes last time we created them */ + public int previousBufferSize = 0; + + /** Width of the dimension in regions last time we created the buffers */ + public int previousRegionWidth = 0; + + /** this is used to prevent multiple threads creating, destroying, or using the buffers at the same time */ + private final ReentrantLock bufferLock = new ReentrantLock(); + + private volatile Box[][] boxCache; + private volatile PosToRenderContainer[][] setsToRender; + private volatile RegionPos center; + + /** + * This is the ChunkPos the player was at the last time the buffers were built. + * IE the center of the buffers last time they were built + */ + private volatile ChunkPos drawableCenterChunkPos = new ChunkPos(0, 0); + private volatile ChunkPos buildableCenterChunkPos = new ChunkPos(0, 0); + + + + + + + public LodBufferBuilder() + { + + } + + /** + * Create a thread to asynchronously generate LOD buffers + * centered around the given camera X and Z. + *
+ * This method will write to the drawable near and far buffers. + *
+ * After the buildable buffers have been generated they must be + * swapped with the drawable buffers in the LodRenderer to be drawn. + */ + public void generateLodBuffersAsync(LodRenderer renderer, LodDimension lodDim, + BlockPos playerBlockPos, boolean fullRegen) + { + + // only allow one generation process to happen at a time + if (generatingBuffers) + return; + + if (buildableBuffers == null) + // setupBuffers hasn't been called yet + return; + + generatingBuffers = true; + + + Thread thread = new Thread(() -> generateLodBuffersThread(renderer, lodDim, playerBlockPos, fullRegen)); + + mainGenThread.execute(thread); + } + + // this was pulled out as a separate method so that it could be + // more easily edited by hot swapping. Because, As far as James is aware + // you can't hot swap lambda expressions. + private void generateLodBuffersThread(LodRenderer renderer, LodDimension lodDim, + BlockPos playerBlockPos, boolean fullRegen) + { + bufferLock.lock(); + + try + { + // round the player's block position down to the nearest chunk BlockPos + ChunkPos playerChunkPos = new ChunkPos(playerBlockPos); + BlockPos playerBlockPosRounded = playerChunkPos.getWorldPosition(); + + + //long startTime = System.currentTimeMillis(); + + ArrayList> nodeToRenderThreads = new ArrayList<>(lodDim.getWidth() * lodDim.getWidth()); + + startBuffers(fullRegen, lodDim); + + + RegionPos playerRegionPos = new RegionPos(playerChunkPos); + if (center == null) + center = playerRegionPos; + + if (setsToRender == null) + setsToRender = new PosToRenderContainer[lodDim.getWidth()][lodDim.getWidth()]; + + if (setsToRender.length != lodDim.getWidth()) + setsToRender = new PosToRenderContainer[lodDim.getWidth()][lodDim.getWidth()]; + + if (boxCache == null) + boxCache = new Box[lodDim.getWidth()][lodDim.getWidth()]; + + if (boxCache.length != lodDim.getWidth()) + boxCache = new Box[lodDim.getWidth()][lodDim.getWidth()]; + + // this will be the center of the VBOs once they have been built + buildableCenterChunkPos = playerChunkPos; + + + //================================// + // create the nodeToRenderThreads // + //================================// + + ClientWorld world = mc.getClientWorld(); + skyLightPlayer = world.getBrightness(LightType.SKY, playerBlockPos); + + for (int xRegion = 0; xRegion < lodDim.getWidth(); xRegion++) + { + for (int zRegion = 0; zRegion < lodDim.getWidth(); zRegion++) + { + if (lodDim.doesRegionNeedBufferRegen(xRegion, zRegion) || fullRegen) + { + RegionPos regionPos = new RegionPos( + xRegion + lodDim.getCenterRegionPosX() - lodDim.getWidth() / 2, + zRegion + lodDim.getCenterRegionPosZ() - lodDim.getWidth() / 2); + + // local position in the vbo and bufferBuilder arrays + BufferBuilder[] currentBuffers = buildableBuffers[xRegion][zRegion]; + LodRegion region = lodDim.getRegion(regionPos.x, regionPos.z); + + if (region == null) + continue; + + // make sure the buffers weren't + // changed while we were running this method + if (currentBuffers == null || !currentBuffers[0].building()) + return; + + byte minDetail = region.getMinDetailLevel(); + + + final int xR = xRegion; + final int zR = zRegion; + + //we create the Callable to use for the buffer builder creation + Callable dataToRenderThread = () -> + { + //Variable initialization + byte detailLevel; + int posX; + int posZ; + int xAdj; + int zAdj; + int bufferIndex; + boolean posNotInPlayerChunk; + boolean adjPosInPlayerChunk; + Box box = ThreadMapUtil.getBox(); + boolean[] adjShadeDisabled = ThreadMapUtil.getAdjShadeDisabledArray(); + + // determine how many LODs we can stack vertically + int maxVerticalData = DetailDistanceUtil.getMaxVerticalData((byte) 0); + + //we get or create the map that will contain the adj data + Map adjData = ThreadMapUtil.getAdjDataArray(maxVerticalData); + + //previous setToRender cache + if (setsToRender[xR][zR] == null) + setsToRender[xR][zR] = new PosToRenderContainer(minDetail, regionPos.x, regionPos.z); + + + //We ask the lod dimension which block we have to render given the player position + PosToRenderContainer posToRender = setsToRender[xR][zR]; + posToRender.clear(minDetail, regionPos.x, regionPos.z); + + lodDim.getPosToRender( + posToRender, + regionPos, + playerBlockPosRounded.getX(), + playerBlockPosRounded.getZ()); + + + + // keep a local version, so we don't have to worry about indexOutOfBounds Exceptions + // if it changes in the LodRenderer while we are working here + boolean[][] vanillaRenderedChunks = renderer.vanillaRenderedChunks; + short gameChunkRenderDistance = (short) (vanillaRenderedChunks.length / 2 - 1); + + + + for (int index = 0; index < posToRender.getNumberOfPos(); index++) + { + bufferIndex = index % currentBuffers.length; + detailLevel = posToRender.getNthDetailLevel(index); + posX = posToRender.getNthPosX(index); + posZ = posToRender.getNthPosZ(index); + + int chunkXdist = LevelPosUtil.getChunkPos(detailLevel, posX) - playerChunkPos.x; + int chunkZdist = LevelPosUtil.getChunkPos(detailLevel, posZ) - playerChunkPos.z; + + //We don't want to render this fake block if + //The block is inside the render distance with, is not bigger than a chunk and is positioned in a chunk set as vanilla rendered + // + //The block is in the player chunk or in a chunk adjacent to the player + if(isThisPositionGoingToBeRendered(detailLevel, posX, posZ, playerChunkPos, vanillaRenderedChunks, gameChunkRenderDistance)) + { + continue; + } + + //we check if the block to render is not in player chunk + posNotInPlayerChunk = !(chunkXdist == 0 && chunkZdist == 0); + + // We extract the adj data in the four cardinal direction + + // we first reset the adjShadeDisabled. This is used to disable the shade on the border when we have transparent block like water or glass + // to avoid having a "darker border" underground + Arrays.fill(adjShadeDisabled, false); + + //We check every adj block in each direction + for (Direction direction : Box.ADJ_DIRECTIONS) + { + + xAdj = posX + Box.DIRECTION_NORMAL_MAP.get(direction).getX(); + zAdj = posZ + Box.DIRECTION_NORMAL_MAP.get(direction).getZ(); + long data; + chunkXdist = LevelPosUtil.getChunkPos(detailLevel, xAdj) - playerChunkPos.x; + chunkZdist = LevelPosUtil.getChunkPos(detailLevel, zAdj) - playerChunkPos.z; + adjPosInPlayerChunk = (chunkXdist == 0 && chunkZdist == 0); + + //If the adj block is rendered in the same region and with same detail + // and is positioned in a place that is not going to be rendered by vanilla game + // then we can set this position as adj + // We avoid cases where the adjPosition is in player chunk while the position is not + // to always have a wall underwater + if(posToRender.contains(detailLevel, xAdj, zAdj) + && !isThisPositionGoingToBeRendered(detailLevel, xAdj, zAdj, playerChunkPos, vanillaRenderedChunks, gameChunkRenderDistance) + && !(posNotInPlayerChunk && adjPosInPlayerChunk)) + { + for (int verticalIndex = 0; verticalIndex < lodDim.getMaxVerticalData(detailLevel, xAdj, zAdj); verticalIndex++) + { + data = lodDim.getData(detailLevel, xAdj, zAdj, verticalIndex); + adjShadeDisabled[Box.DIRECTION_INDEX.get(direction)] = false; + adjData.get(direction)[verticalIndex] = data; + } + } + else + { + //Otherwise, we check if this position is + data = lodDim.getSingleData(detailLevel, xAdj, zAdj); + + adjData.get(direction)[0] = DataPointUtil.EMPTY_DATA; + + if ((isThisPositionGoingToBeRendered(detailLevel, xAdj, zAdj, playerChunkPos, vanillaRenderedChunks, gameChunkRenderDistance) || (posNotInPlayerChunk && adjPosInPlayerChunk)) + && !DataPointUtil.isVoid(data)) + { + adjShadeDisabled[Box.DIRECTION_INDEX.get(direction)] = DataPointUtil.getAlpha(data) < 255; + } + } + } + + + // We render every vertical lod present in this position + // We only stop when we find a block that is void or non existing block + long data; + for (int verticalIndex = 0; verticalIndex < lodDim.getMaxVerticalData(detailLevel, posX, posZ); verticalIndex++) + { + + //we get the above block as adj UP + if (verticalIndex > 0) + adjData.get(Direction.UP)[0] = lodDim.getData(detailLevel, posX, posZ, verticalIndex - 1); + else + adjData.get(Direction.UP)[0] = DataPointUtil.EMPTY_DATA; + + + //we get the below block as adj DOWN + if (verticalIndex < lodDim.getMaxVerticalData(detailLevel, posX, posZ) - 1) + adjData.get(Direction.DOWN)[0] = lodDim.getData(detailLevel, posX, posZ, verticalIndex + 1); + else + adjData.get(Direction.DOWN)[0] = DataPointUtil.EMPTY_DATA; + + //We extract the data to render + data = lodDim.getData(detailLevel, posX, posZ, verticalIndex); + + //If the data is not renderable (Void or non existing) we stop since there is no data left in this position + if (DataPointUtil.isVoid(data) || !DataPointUtil.doesItExist(data)) + break; + + //We send the call to create the vertices + LodConfig.CLIENT.graphics.advancedGraphicsOption.lodTemplate.get().template.addLodToBuffer(currentBuffers[bufferIndex], playerBlockPosRounded, data, adjData, + detailLevel, posX, posZ, box, renderer.previousDebugMode, adjShadeDisabled); + } + + } // for pos to in list to render + // the thread executed successfully + return true; + }; + + nodeToRenderThreads.add(dataToRenderThread); + + } + } // region z + } // region z + + + //long executeStart = System.currentTimeMillis(); + // wait for all threads to finish + List> futuresBuffer = bufferBuilderThreads.invokeAll(nodeToRenderThreads); + for (Future future : futuresBuffer) + { + // the future will be false if its thread failed + if (!future.get()) + { + ClientProxy.LOGGER.warn("LodBufferBuilder ran into trouble and had to start over."); + break; + } + } + //long executeEnd = System.currentTimeMillis(); + + + //long endTime = System.currentTimeMillis(); + //long buildTime = endTime - startTime; + //long executeTime = executeEnd - executeStart; + +// ClientProxy.LOGGER.info("Thread Build time: " + buildTime + " ms" + '\n' + +// "thread execute time: " + executeTime + " ms"); + + // mark that the buildable buffers as ready to swap + switchVbos = true; + } + catch (Exception e) + { + ClientProxy.LOGGER.warn("\"LodNodeBufferBuilder.generateLodBuffersAsync\" ran into trouble: "); + e.printStackTrace(); + } + finally + { + // clean up any potentially open resources + if (buildableBuffers != null) + closeBuffers(fullRegen, lodDim); + + try + { + // upload the new buffers + uploadBuffers(fullRegen, lodDim); + } + catch (Exception e) + { + ClientProxy.LOGGER.warn("\"LodNodeBufferBuilder.generateLodBuffersAsync\" was unable to upload the buffers to the GPU: " + e.getMessage()); + e.printStackTrace(); + } + + // regardless of whether we were able to successfully create + // the buffers, we are done generating. + generatingBuffers = false; + bufferLock.unlock(); + } + } + + private boolean isThisPositionGoingToBeRendered(byte detailLevel, int posX, int posZ, ChunkPos playerChunkPos, boolean[][] vanillaRenderedChunks, int gameChunkRenderDistance){ + + + // skip any chunks that Minecraft is going to render + int chunkXdist = LevelPosUtil.getChunkPos(detailLevel, posX) - playerChunkPos.x; + int chunkZdist = LevelPosUtil.getChunkPos(detailLevel, posZ) - playerChunkPos.z; + + // check if the chunk is on the border + boolean isItBorderPos; + if (LodConfig.CLIENT.graphics.advancedGraphicsOption.vanillaOverdraw.get() == VanillaOverdraw.BORDER) + isItBorderPos = LodUtil.isBorderChunk(vanillaRenderedChunks, chunkXdist + gameChunkRenderDistance + 1, chunkZdist + gameChunkRenderDistance + 1); + else + isItBorderPos = false; + + + //boolean smallRenderDistance = gameChunkRenderDistance <= LodUtil.MINIMUM_RENDER_DISTANCE_FOR_PARTIAL_OVERDRAW; + + // get the positions that will be rendered + + return (gameChunkRenderDistance >= Math.abs(chunkXdist) + && gameChunkRenderDistance >= Math.abs(chunkZdist) + && detailLevel <= LodUtil.CHUNK_DETAIL_LEVEL + && vanillaRenderedChunks[chunkXdist + gameChunkRenderDistance + 1][chunkZdist + gameChunkRenderDistance + 1]) + && (!isItBorderPos); + } + + + + + + + //===============================// + // BufferBuilder related methods // + //===============================// + + /** + * Called from the LodRenderer to create the + * BufferBuilders.

+ *

+ * May have to wait for the bufferLock to open. + */ + public void setupBuffers(LodDimension lodDimension) + { + GlProxy glProxy = GlProxy.getInstance(); + + bufferLock.lock(); + int numbRegionsWide = lodDimension.getWidth(); + long regionMemoryRequired; + int numberOfBuffers; + + previousRegionWidth = numbRegionsWide; + numberOfBuffersPerRegion = new int[numbRegionsWide][numbRegionsWide]; + buildableBuffers = new BufferBuilder[numbRegionsWide][numbRegionsWide][]; + + buildableVbos = new VertexBuffer[numbRegionsWide][numbRegionsWide][]; + drawableVbos = new VertexBuffer[numbRegionsWide][numbRegionsWide][]; + + if (glProxy.bufferStorageSupported) + { + buildableStorageBufferIds = new int[numbRegionsWide][numbRegionsWide][]; + drawableStorageBufferIds = new int[numbRegionsWide][numbRegionsWide][]; + } + + for (int x = 0; x < numbRegionsWide; x++) + { + for (int z = 0; z < numbRegionsWide; z++) + { + regionMemoryRequired = DEFAULT_MEMORY_ALLOCATION; + + // if the memory required is greater than the max buffer + // capacity, divide the memory across multiple buffers + if (regionMemoryRequired > LodUtil.MAX_ALLOCATABLE_DIRECT_MEMORY) + { + numberOfBuffers = (int) regionMemoryRequired / LodUtil.MAX_ALLOCATABLE_DIRECT_MEMORY + 1; + + // TODO shouldn't this be determined with regionMemoryRequired? + // always allocating the max memory is a bit expensive isn't it? + regionMemoryRequired = LodUtil.MAX_ALLOCATABLE_DIRECT_MEMORY; + numberOfBuffersPerRegion[x][z] = numberOfBuffers; + buildableBuffers[x][z] = new BufferBuilder[numberOfBuffers]; + buildableVbos[x][z] = new VertexBuffer[numberOfBuffers]; + drawableVbos[x][z] = new VertexBuffer[numberOfBuffers]; + + if (glProxy.bufferStorageSupported) + { + buildableStorageBufferIds[x][z] = new int[numberOfBuffers]; + drawableStorageBufferIds[x][z] = new int[numberOfBuffers]; + } + } + else + { + // we only need one buffer for this region + numberOfBuffersPerRegion[x][z] = 1; + buildableBuffers[x][z] = new BufferBuilder[1]; + buildableVbos[x][z] = new VertexBuffer[1]; + drawableVbos[x][z] = new VertexBuffer[1]; + + if (glProxy.bufferStorageSupported) + { + buildableStorageBufferIds[x][z] = new int[1]; + drawableStorageBufferIds[x][z] = new int[1]; + } + } + + + for (int i = 0; i < numberOfBuffersPerRegion[x][z]; i++) + { + buildableBuffers[x][z][i] = new BufferBuilder((int) regionMemoryRequired); + + buildableVbos[x][z][i] = new VertexBuffer(LodUtil.LOD_VERTEX_FORMAT); + drawableVbos[x][z][i] = new VertexBuffer(LodUtil.LOD_VERTEX_FORMAT); + + + // create the initial mapped buffers (system memory) + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, buildableVbos[x][z][i].id); + GL15.glBufferData(GL15.GL_ARRAY_BUFFER, regionMemoryRequired, GL15.GL_DYNAMIC_DRAW); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, drawableVbos[x][z][i].id); + GL15.glBufferData(GL15.GL_ARRAY_BUFFER, regionMemoryRequired, GL15.GL_DYNAMIC_DRAW); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + + + if (glProxy.bufferStorageSupported) + { + // create the buffer storage (GPU memory) + buildableStorageBufferIds[x][z][i] = GL15.glGenBuffers(); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, buildableStorageBufferIds[x][z][i]); + GL45.glBufferStorage(GL15.GL_ARRAY_BUFFER, regionMemoryRequired, 0); // the 0 flag means to create the storage in the GPUs memory + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + + drawableStorageBufferIds[x][z][i] = GL15.glGenBuffers(); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, drawableStorageBufferIds[x][z][i]); + GL45.glBufferStorage(GL15.GL_ARRAY_BUFFER, regionMemoryRequired, 0); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + } + } + } + } + + bufferLock.unlock(); + } + + + + /** + * Sets the buffers and Vbos to null, forcing them to be recreated
+ * and destroys any bound OpenGL objects.

+ *

+ * May have to wait for the bufferLock to open. + */ + public void destroyBuffers() + { + bufferLock.lock(); + + + // destroy the buffer storages if they aren't already + if (buildableStorageBufferIds != null) + { + for (int x = 0; x < buildableStorageBufferIds.length; x++) + { + for (int z = 0; z < buildableStorageBufferIds.length; z++) + { + for (int i = 0; i < buildableStorageBufferIds[x][z].length; i++) + { + int buildableId = buildableStorageBufferIds[x][z][i]; + int drawableId = drawableStorageBufferIds[x][z][i]; + + // Send this over to the render thread, if this is being + // called we aren't worried about stuttering anyway. + // This way we don't have to worry about what context this + // was called from (if any). + RenderSystem.recordRenderCall(() -> + { + GL15.glDeleteBuffers(buildableId); + GL15.glDeleteBuffers(drawableId); + }); + } + } + } + } + + buildableStorageBufferIds = null; + drawableStorageBufferIds = null; + + + + + // destroy the VBOs if they aren't already + if (buildableVbos != null) + { + for (int i = 0; i < buildableVbos.length; i++) + { + for (int j = 0; j < buildableVbos.length; j++) + { + for (int k = 0; k < buildableVbos[i][j].length; k++) + { + int buildableId; + int drawableId; + + // variables passed into a lambda expression + // need to be effectively final, so we have + // to use an else statement here + if (buildableVbos[i][j][k] != null) + buildableId = buildableVbos[i][j][k].id; + else + buildableId = 0; + + if (drawableVbos[i][j][k] != null) + drawableId = drawableVbos[i][j][k].id; + else + drawableId = 0; + + + RenderSystem.recordRenderCall(() -> + { + if (buildableId != 0) + GL15.glDeleteBuffers(buildableId); + if (drawableId != 0) + GL15.glDeleteBuffers(drawableId); + }); + } + } + } + } + + buildableVbos = null; + drawableVbos = null; + + + // these don't contain any OpenGL objects, so + // they don't require any special clean-up + buildableBuffers = null; + + bufferLock.unlock(); + } + + /** Calls begin on each of the buildable BufferBuilders. */ + private void startBuffers(boolean fullRegen, LodDimension lodDim) + { + for (int x = 0; x < buildableBuffers.length; x++) + { + for (int z = 0; z < buildableBuffers.length; z++) + { + if (fullRegen || lodDim.doesRegionNeedBufferRegen(x, z)) + { + for (int i = 0; i < buildableBuffers[x][z].length; i++) + { + // for some reason BufferBuilder.vertexCounts + // isn't reset unless this is called, which can cause + // a false indexOutOfBoundsException + buildableBuffers[x][z][i].discard(); + + buildableBuffers[x][z][i].begin(GL11.GL_QUADS, LodUtil.LOD_VERTEX_FORMAT); + } + } + } + } + } + + /** Calls end on each of the buildable BufferBuilders. */ + private void closeBuffers(boolean fullRegen, LodDimension lodDim) + { + for (int x = 0; x < buildableBuffers.length; x++) + for (int z = 0; z < buildableBuffers.length; z++) + for (int i = 0; i < buildableBuffers[x][z].length; i++) + if (buildableBuffers[x][z][i] != null && buildableBuffers[x][z][i].building() && (fullRegen || lodDim.doesRegionNeedBufferRegen(x, z))) + buildableBuffers[x][z][i].end(); + } + + + /** Upload all buildableBuffers to the GPU. */ + private void uploadBuffers(boolean fullRegen, LodDimension lodDim) + { + GlProxy glProxy = GlProxy.getInstance(); + + try + { + // make sure we are uploading to the builder context, + // this helps prevent interference (IE stuttering) with the Minecraft context. + glProxy.setGlContext(GlProxyContext.LOD_BUILDER); + + // determine the upload method + GpuUploadMethod uploadMethod = LodConfig.CLIENT.graphics.advancedGraphicsOption.gpuUploadMethod.get(); + if (!glProxy.bufferStorageSupported && uploadMethod == GpuUploadMethod.BUFFER_STORAGE) + { + // if buffer storage isn't supported + // default to SUB_DATA + LodConfig.CLIENT.graphics.advancedGraphicsOption.gpuUploadMethod.set(GpuUploadMethod.SUB_DATA); + uploadMethod = GpuUploadMethod.SUB_DATA; + } + + // actually upload the buffers + for (int x = 0; x < buildableVbos.length; x++) + { + for (int z = 0; z < buildableVbos.length; z++) + { + if (fullRegen || lodDim.doesRegionNeedBufferRegen(x, z)) + { + for (int i = 0; i < buildableBuffers[x][z].length; i++) + { + ByteBuffer uploadBuffer = buildableBuffers[x][z][i].popNextBuffer().getSecond(); + vboUpload(buildableVbos[x][z][i], buildableStorageBufferIds[x][z][i], uploadBuffer, true, uploadMethod); + lodDim.setRegenRegionBufferByArrayIndex(x, z, false); + } + } + } + } + } + catch (Exception e) + { + // this doesn't appear to be necessary anymore, but just in case. + ClientProxy.LOGGER.error(LodBufferBuilder.class.getSimpleName() + " - UploadBuffers failed: " + e.getMessage()); + e.printStackTrace(); + } + finally + { + GL11.glFinish(); + + // close the context so it can be re-used later. + // I'm guessing we can't just leave it because the executor service + // does something that invalidates the OpenGL context. + glProxy.setGlContext(GlProxyContext.NONE); + } + } + + /** Uploads the uploadBuffer so the GPU can use it. */ + private void vboUpload(VertexBuffer vbo, int storageBufferId, ByteBuffer uploadBuffer, + boolean allowBufferExpansion, GpuUploadMethod uploadMethod) + { + // this shouldn't happen, but just to be safe + if (vbo.id != -1 && GlProxy.getInstance().getGlContext() == GlProxyContext.LOD_BUILDER) + { + // this is how many points will be rendered + vbo.vertexCount = (uploadBuffer.capacity() / vbo.format.getVertexSize()); + + + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo.id); + try + { + // if possible use the faster buffer storage route + if (uploadMethod == GpuUploadMethod.BUFFER_STORAGE) + { + // get a pointer to the buffer in system memory + ByteBuffer vboBuffer = GL30.glMapBufferRange(GL15.GL_ARRAY_BUFFER, 0, uploadBuffer.capacity(), GL30.GL_MAP_WRITE_BIT | GL30.GL_MAP_UNSYNCHRONIZED_BIT); + if (vboBuffer == null) + { + int previousCapacity = uploadBuffer.capacity(); + + // only expand the buffers if the uploadBuffer actually + // has something in it and expansion is allowed + if (previousCapacity != 0 && allowBufferExpansion) + { + // the buffer(s) aren't big enough, expand them. + // This does cause lag/stuttering, so it should be avoided! + + // expand the buffer in system memory + GL15.glBufferData(GL15.GL_ARRAY_BUFFER, (int) (uploadBuffer.capacity() * BUFFER_EXPANSION_MULTIPLIER), GL15.GL_DYNAMIC_DRAW); + GL15.glBufferSubData(GL15.GL_ARRAY_BUFFER, 0, uploadBuffer); + + // un-bind the system memory buffer + GL15.glUnmapBuffer(GL15.GL_ARRAY_BUFFER); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + + // expand the buffer storage + GL15.glDeleteBuffers(storageBufferId); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, storageBufferId); + GL45.glBufferStorage(GL15.GL_ARRAY_BUFFER, (int) (uploadBuffer.capacity() * BUFFER_EXPANSION_MULTIPLIER), 0); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + + + // recursively try to upload into the newly created buffer storage + // but don't recurse again if that fails + // (we don't want an infinitely expanding buffer!) + vboUpload(vbo, storageBufferId, uploadBuffer, false, uploadMethod); + } + } + else + { + // upload the buffer into system memory... + vboBuffer.put(uploadBuffer); + GL15.glUnmapBuffer(GL15.GL_ARRAY_BUFFER); + + // ...then upload into GPU memory + // (uploading into GPU memory directly can only be done + // through the glCopyBufferSubData/glCopyNamed... methods) + GL45.glCopyNamedBufferSubData(vbo.id, storageBufferId, 0, 0, uploadBuffer.capacity()); + } + } + else if (uploadMethod == GpuUploadMethod.BUFFER_MAPPING) + { + // no stuttering but high GPU usage + // stores everything in system memory instead of GPU memory + // making rendering much slower. + // Unless the user is running integrated graphics, + // in that case this will actually work better than SUB_DATA. + + + ByteBuffer vboBuffer; + + // map buffer range is better since it can be explicitly unsynchronized + if (GlProxy.getInstance().mapBufferRangeSupported) + vboBuffer = GL30.glMapBufferRange(GL30.GL_ARRAY_BUFFER, 0, uploadBuffer.capacity(), GL30.GL_MAP_WRITE_BIT | GL30.GL_MAP_UNSYNCHRONIZED_BIT); + else + vboBuffer = GL15.glMapBuffer(GL30.GL_ARRAY_BUFFER, uploadBuffer.capacity()); + + + if (vboBuffer == null) + { + GL15.glBufferData(GL45.GL_ARRAY_BUFFER, (int) (uploadBuffer.capacity() * BUFFER_EXPANSION_MULTIPLIER), GL15.GL_DYNAMIC_DRAW); + GL15.glBufferSubData(GL15.GL_ARRAY_BUFFER, 0, uploadBuffer); + } + else + { + vboBuffer.put(uploadBuffer); + } + } + else + { + // hybrid subData/bufferData // + // less stutter, low GPU usage + + //long size = GL31.glGetBufferParameteri64(GL15.GL_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE); // hopefully just a int should be long enough + long size = GL15.glGetBufferParameteri(GL15.GL_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE); + if (size < uploadBuffer.capacity() * BUFFER_EXPANSION_MULTIPLIER) + { + GL15.glBufferData(GL15.GL_ARRAY_BUFFER, (int) (uploadBuffer.capacity() * BUFFER_EXPANSION_MULTIPLIER), GL15.GL_DYNAMIC_DRAW); + } + GL15.glBufferSubData(GL15.GL_ARRAY_BUFFER, 0, uploadBuffer); + } + } + catch (Exception e) + { + ClientProxy.LOGGER.error("vboUpload failed: " + e.getClass().getSimpleName()); + e.printStackTrace(); + } + finally + { + if (uploadMethod == GpuUploadMethod.BUFFER_MAPPING) + GL15.glUnmapBuffer(GL15.GL_ARRAY_BUFFER); + + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + } + + }//if vbo exists and in correct GL context + }//vboUpload + + /** Get the newly created VBOs */ + public VertexBuffersAndOffset getVertexBuffers() + { + // don't wait for the lock to open, + // since this is called on the main render thread + if (bufferLock.tryLock()) + { + VertexBuffer[][][] tmpVbo = drawableVbos; + drawableVbos = buildableVbos; + buildableVbos = tmpVbo; + + int[][][] tmpStorage = drawableStorageBufferIds; + drawableStorageBufferIds = buildableStorageBufferIds; + buildableStorageBufferIds = tmpStorage; + + drawableCenterChunkPos = buildableCenterChunkPos; + + // the vbos have been swapped + switchVbos = false; + bufferLock.unlock(); + } + + return new VertexBuffersAndOffset(drawableVbos, drawableStorageBufferIds, drawableCenterChunkPos); + } + + /** A simple container to pass multiple objects back in the getVertexBuffers method. */ + public static class VertexBuffersAndOffset + { + public final VertexBuffer[][][] vbos; + public final int[][][] storageBufferIds; + public final ChunkPos drawableCenterChunkPos; + + public VertexBuffersAndOffset(VertexBuffer[][][] newVbos, int[][][] newStorageBufferIds, ChunkPos newDrawableCenterChunkPos) + { + vbos = newVbos; + storageBufferIds = newStorageBufferIds; + drawableCenterChunkPos = newDrawableCenterChunkPos; + } + } + + /** + * If this is true the buildable near and far + * buffers have been generated and are ready to be + * sent to the LodRenderer. + */ + public boolean newBuffersAvailable() + { + return switchVbos; + } +} diff --git a/src/main/java/com/seibel/lod/builders/bufferBuilding/lodTemplates/AbstractLodTemplate.java b/src/main/java/com/seibel/lod/builders/bufferBuilding/lodTemplates/AbstractLodTemplate.java new file mode 100644 index 000000000..daff1fa0f --- /dev/null +++ b/src/main/java/com/seibel/lod/builders/bufferBuilding/lodTemplates/AbstractLodTemplate.java @@ -0,0 +1,53 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.builders.bufferBuilding.lodTemplates; + +import java.util.Map; + +import com.seibel.lod.enums.DebugMode; +import com.seibel.lod.util.ColorUtil; + +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.util.Direction; +import net.minecraft.util.math.BlockPos; + +/** + * This is the abstract class used to create different + * BufferBuilders. + * @author James Seibel + * @version 10-10-2021 + */ +public abstract class AbstractLodTemplate +{ + + /** Uploads the given LOD to the buffer. */ + public abstract void addLodToBuffer(BufferBuilder buffer, BlockPos bufferCenterBlockPos, long data, Map adjData, + byte detailLevel, int posX, int posZ, Box box, DebugMode debugging, boolean[] adjShadeDisabled); + + /** add the given position and color to the buffer */ + protected void addPosAndColor(BufferBuilder buffer, + double x, double y, double z, + int color) + { + // TODO re-add transparency by replacing the 255 with "ColorUtil.getAlpha(color)" + buffer.vertex(x, y, z).color(ColorUtil.getRed(color), ColorUtil.getGreen(color), ColorUtil.getBlue(color), 255).endVertex(); + } + +} diff --git a/src/main/java/com/seibel/lod/builders/bufferBuilding/lodTemplates/Box.java b/src/main/java/com/seibel/lod/builders/bufferBuilding/lodTemplates/Box.java new file mode 100644 index 000000000..90172d0a7 --- /dev/null +++ b/src/main/java/com/seibel/lod/builders/bufferBuilding/lodTemplates/Box.java @@ -0,0 +1,595 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.builders.bufferBuilding.lodTemplates; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import com.seibel.lod.config.LodConfig; +import com.seibel.lod.enums.DebugMode; +import com.seibel.lod.util.ColorUtil; +import com.seibel.lod.util.DataPointUtil; +import com.seibel.lod.util.LodUtil; +import com.seibel.lod.wrappers.MinecraftWrapper; + +import net.minecraft.util.Direction; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.vector.Vector3i; + +/** + * Similar to Minecraft's AxisAlignedBoundingBox. + * @author Leonardo Amato + * @version 10-2-2021 + */ +public class Box +{ + + public static final int ADJACENT_HEIGHT_INDEX = 0; + public static final int ADJACENT_DEPTH_INDEX = 1; + + public static final int X = 0; + public static final int Y = 1; + public static final int Z = 2; + + public static final int MIN = 0; + public static final int MAX = 1; + + public static final int VOID_FACE = 0; + + /** The six cardinal directions */ + public static final Direction[] DIRECTIONS = new Direction[] { + Direction.UP, + Direction.DOWN, + Direction.WEST, + Direction.EAST, + Direction.NORTH, + Direction.SOUTH }; + + /** North, South, East, West */ + public static final Direction[] ADJ_DIRECTIONS = new Direction[] { + Direction.EAST, + Direction.WEST, + Direction.SOUTH, + Direction.NORTH }; + + /** All the faces and vertices of a cube. This is used to extract the vertex from the column */ + public static final Map DIRECTION_VERTEX_MAP = new HashMap() + {{ + put(Direction.UP, new int[][] { + { 0, 1, 0 }, + { 0, 1, 1 }, + { 1, 1, 1 }, + { 1, 1, 0 } }); + put(Direction.DOWN, new int[][] { + { 1, 0, 0 }, + { 1, 0, 1 }, + { 0, 0, 1 }, + { 0, 0, 0 } }); + put(Direction.EAST, new int[][] { + { 1, 1, 0 }, + { 1, 1, 1 }, + { 1, 0, 1 }, + { 1, 0, 0 } }); + put(Direction.WEST, new int[][] { + { 0, 0, 0 }, + { 0, 0, 1 }, + { 0, 1, 1 }, + { 0, 1, 0 } }); + put(Direction.SOUTH, new int[][] { + { 1, 0, 1 }, + { 1, 1, 1 }, + { 0, 1, 1 }, + { 0, 0, 1 } }); + put(Direction.NORTH, new int[][] { + { 0, 0, 0 }, + { 0, 1, 0 }, + { 1, 1, 0 }, + { 1, 0, 0 } }); + }}; + + + /** + * This indicates which position is invariable in the DIRECTION_VERTEX_MAP. + * Is used to extract the vertex + */ + public static final Map FACE_DIRECTION = new HashMap() + {{ + put(Direction.UP, new int[] { Y, MAX }); + put(Direction.DOWN, new int[] { Y, MIN }); + put(Direction.EAST, new int[] { X, MAX }); + put(Direction.WEST, new int[] { X, MIN }); + put(Direction.SOUTH, new int[] { Z, MAX }); + put(Direction.NORTH, new int[] { Z, MIN }); + }}; + + + /** + * This is a map from Direction to the relative normal vector + * we are using this since I'm not sure if the getNormal create new object at every call + */ + public static final Map DIRECTION_NORMAL_MAP = new HashMap() + {{ + put(Direction.UP, Direction.UP.getNormal()); + put(Direction.DOWN, Direction.DOWN.getNormal()); + put(Direction.EAST, Direction.EAST.getNormal()); + put(Direction.WEST, Direction.WEST.getNormal()); + put(Direction.SOUTH, Direction.SOUTH.getNormal()); + put(Direction.NORTH, Direction.NORTH.getNormal()); + }}; + + /** We use this index for all array that are going to */ + public static final Map DIRECTION_INDEX = new HashMap() + {{ + put(Direction.UP, 0); + put(Direction.DOWN, 1); + put(Direction.EAST, 2); + put(Direction.WEST, 3); + put(Direction.SOUTH, 4); + put(Direction.NORTH, 5); + }}; + + public static final Map ADJ_DIRECTION_INDEX = new HashMap() + {{ + put(Direction.EAST, 0); + put(Direction.WEST, 1); + put(Direction.SOUTH, 2); + put(Direction.NORTH, 3); + }}; + /** holds the box's x, y, z offset */ + public final int[] boxOffset; + /** holds the box's x, y, z width */ + public final int[] boxWidth; + + /** Holds each direction's color */ + public final int[] colorMap; + /** The original color (before shading) of this box */ + public int color; + /** + * + */ + public final Map adjHeight; + public final Map adjDepth; + public final Map skyLights; + public byte blockLight; + + /** Holds if the given direction should be culled or not */ + public final boolean[] culling; + + + /** creates an empty box */ + public Box() + { + boxOffset = new int[3]; + boxWidth = new int[3]; + + colorMap = new int[6]; + skyLights = new HashMap() + {{ + put(Direction.UP, new byte[1]); + put(Direction.DOWN, new byte[1]); + put(Direction.EAST, new byte[LodUtil.MAX_NUMBER_OF_VERTICAL_LODS]); + put(Direction.WEST, new byte[LodUtil.MAX_NUMBER_OF_VERTICAL_LODS]); + put(Direction.SOUTH, new byte[LodUtil.MAX_NUMBER_OF_VERTICAL_LODS]); + put(Direction.NORTH, new byte[LodUtil.MAX_NUMBER_OF_VERTICAL_LODS]); + }}; + adjHeight = new HashMap() + {{ + put(Direction.EAST, new int[LodUtil.MAX_NUMBER_OF_VERTICAL_LODS]); + put(Direction.WEST, new int[LodUtil.MAX_NUMBER_OF_VERTICAL_LODS]); + put(Direction.SOUTH, new int[LodUtil.MAX_NUMBER_OF_VERTICAL_LODS]); + put(Direction.NORTH, new int[LodUtil.MAX_NUMBER_OF_VERTICAL_LODS]); + }}; + adjDepth = new HashMap() + {{ + put(Direction.EAST, new int[LodUtil.MAX_NUMBER_OF_VERTICAL_LODS]); + put(Direction.WEST, new int[LodUtil.MAX_NUMBER_OF_VERTICAL_LODS]); + put(Direction.SOUTH, new int[LodUtil.MAX_NUMBER_OF_VERTICAL_LODS]); + put(Direction.NORTH, new int[LodUtil.MAX_NUMBER_OF_VERTICAL_LODS]); + }}; + + culling = new boolean[6]; + } + + /** Set the light of the columns */ + public void setLights(int skyLight, int blockLight) + { + this.blockLight = (byte) blockLight; + skyLights.get(Direction.UP)[0] = (byte) skyLight; + } + + /** + * Set the color of the columns + * @param color color to add + * @param adjShadeDisabled this array indicates which face does not need shading + */ + public void setColor(int color, boolean[] adjShadeDisabled) + { + this.color = color; + for (Direction direction : DIRECTIONS) + { + if (!adjShadeDisabled[DIRECTION_INDEX.get(direction)]) + colorMap[DIRECTION_INDEX.get(direction)] = ColorUtil.applyShade(color, MinecraftWrapper.INSTANCE.getClientWorld().getShade(direction, true)); + else + colorMap[DIRECTION_INDEX.get(direction)] = color; + } + } + + /** + * @param direction of the face of which we want to get the color + * @return color of the face + */ + public int getColor(Direction direction) + { + if (LodConfig.CLIENT.advancedModOptions.debugging.debugMode.get() != DebugMode.SHOW_DETAIL) + return colorMap[DIRECTION_INDEX.get(direction)]; + else + return ColorUtil.applyShade(color, MinecraftWrapper.INSTANCE.getClientWorld().getShade(direction, true)); + } + + /** + */ + public byte getSkyLight(Direction direction, int verticalIndex) + { + if(direction == Direction.UP || direction == Direction.DOWN) + return skyLights.get(direction)[0]; + else + return skyLights.get(direction)[verticalIndex]; + } + + /** + */ + public int getBlockLight() + { + return blockLight; + } + /** clears this box, resetting everything to default values */ + public void reset() + { + Arrays.fill(boxWidth, 0); + Arrays.fill(boxOffset, 0); + Arrays.fill(colorMap, 0); + blockLight = 0; + for (Direction direction : ADJ_DIRECTIONS) + { + for (int i = 0; i < adjHeight.get(direction).length; i++) + { + adjHeight.get(direction)[i] = VOID_FACE; + adjDepth.get(direction)[i] = VOID_FACE; + skyLights.get(direction)[i] = 0; + } + } + } + + /** determine which faces should be culled */ + public void setUpCulling(int cullingDistance, BlockPos playerPos) + { + for (Direction direction : DIRECTIONS) + { + if (direction == Direction.DOWN || direction == Direction.WEST || direction == Direction.NORTH) + culling[DIRECTION_INDEX.get(direction)] = playerPos.get(direction.getAxis()) > getFacePos(direction) + cullingDistance; + + else if (direction == Direction.UP || direction == Direction.EAST || direction == Direction.SOUTH) + culling[DIRECTION_INDEX.get(direction)] = playerPos.get(direction.getAxis()) < getFacePos(direction) - cullingDistance; + + culling[DIRECTION_INDEX.get(direction)] = false; + } + } + + /** + * @param direction direction that we want to check if it's culled + * @return true if and only if the face of the direction is culled + */ + public boolean isCulled(Direction direction) + { + return culling[DIRECTION_INDEX.get(direction)]; + } + + + /** + * This method create all the shared face culling based on the adjacent data + * @param adjData data adjacent to the column we are going to render + */ + public void setAdjData(Map adjData) + { + int height; + int depth; + int minY = getMinY(); + int maxY = getMaxY(); + long singleAdjDataPoint; + + /* TODO implement attached vertical face culling + //Up direction case + if(DataPointUtil.doesItExist(adjData.get(Direction.UP))) + { + height = DataPointUtil.getHeight(singleAdjDataPoint); + depth = DataPointUtil.getDepth(singleAdjDataPoint); + }*/ + //Down direction case + singleAdjDataPoint = adjData.get(Direction.DOWN)[0]; + if(DataPointUtil.doesItExist(singleAdjDataPoint)) + skyLights.get(Direction.DOWN)[0] = DataPointUtil.getLightSkyAlt(singleAdjDataPoint); + else + skyLights.get(Direction.DOWN)[0] = skyLights.get(Direction.UP)[0]; + //other sided + //TODO clean some similar cases + for (Direction direction : ADJ_DIRECTIONS) + { + if (isCulled(direction)) + continue; + + long[] dataPoint = adjData.get(direction); + if (dataPoint == null || DataPointUtil.isVoid(dataPoint[0])) + { + adjHeight.get(direction)[0] = maxY; + adjDepth.get(direction)[0] = minY; + adjHeight.get(direction)[1] = VOID_FACE; + adjDepth.get(direction)[1] = VOID_FACE; + skyLights.get(direction)[0] = 15; //in void set full sky light + continue; + } + + int i; + int faceToDraw = 0; + boolean firstFace = true; + boolean toFinish = false; + int toFinishIndex = 0; + boolean allAbove = true; + for (i = 0; i < dataPoint.length; i++) + { + singleAdjDataPoint = dataPoint[i]; + + if (DataPointUtil.isVoid(singleAdjDataPoint) || !DataPointUtil.doesItExist(singleAdjDataPoint)) + break; + + height = DataPointUtil.getHeight(singleAdjDataPoint); + depth = DataPointUtil.getDepth(singleAdjDataPoint); + + if (depth <= maxY) + { + allAbove = false; + if (height < minY) + { + // the adj data is lower than the current data + + if (firstFace) + { + adjHeight.get(direction)[0] = getMaxY(); + adjDepth.get(direction)[0] = getMinY(); + skyLights.get(direction)[0] = DataPointUtil.getLightSkyAlt(singleAdjDataPoint); //skyLights.get(Direction.UP)[0]; + } + else + { + adjDepth.get(direction)[faceToDraw] = getMinY(); + skyLights.get(direction)[faceToDraw] = DataPointUtil.getLightSkyAlt(singleAdjDataPoint); + } + faceToDraw++; + toFinish = false; + + // break since all the other data will be lower + break; + } + else if (depth <= minY) + { + if (height >= maxY) + { + // the adj data is inside the current data + // don't draw the face + adjHeight.get(direction)[0] = VOID_FACE; + adjDepth.get(direction)[0] = VOID_FACE; + } + else // height < maxY + { + // the adj data intersects the lower part of the current data + // if this is the only face, use the maxY and break, + // if there was another face we finish the last one and break + if (firstFace) + { + adjHeight.get(direction)[0] = getMaxY(); + adjDepth.get(direction)[0] = height; + skyLights.get(direction)[0] = DataPointUtil.getLightSkyAlt(singleAdjDataPoint); //skyLights.get(Direction.UP)[0]; + } + else + { + adjDepth.get(direction)[faceToDraw] = height; + skyLights.get(direction)[faceToDraw] = DataPointUtil.getLightSkyAlt(singleAdjDataPoint); + } + toFinish = false; + faceToDraw++; + } + break; + } + else if (height >= maxY)//depth > minY && + { + // the adj data intersects the higher part of the current data + // we start the creation of a new face + adjHeight.get(direction)[faceToDraw] = depth; + //skyLights.get(direction)[faceToDraw] = (byte) DataPointUtil.getLightSkyAlt(singleAdjDataPoint); + firstFace = false; + toFinish = true; + toFinishIndex = i + 1; + } + else + { + // if (depth > minY && height < maxY) + + // the adj data is contained in the current data + if (firstFace) + { + adjHeight.get(direction)[0] = getMaxY(); + } + + adjDepth.get(direction)[faceToDraw] = height; + skyLights.get(direction)[faceToDraw] = DataPointUtil.getLightSkyAlt(singleAdjDataPoint); + faceToDraw++; + adjHeight.get(direction)[faceToDraw] = depth; + firstFace = false; + toFinish = true; + toFinishIndex = i + 1; + } + } + } + + if (allAbove) + { + adjHeight.get(direction)[0] = getMaxY(); + adjDepth.get(direction)[0] = getMinY(); + skyLights.get(direction)[0] = skyLights.get(Direction.UP)[0]; + faceToDraw++; + } + else if (toFinish) + { + adjDepth.get(direction)[faceToDraw] = minY; + if(toFinishIndex < dataPoint.length) + { + singleAdjDataPoint = dataPoint[toFinishIndex]; + if (DataPointUtil.doesItExist(singleAdjDataPoint)) + skyLights.get(direction)[faceToDraw] = DataPointUtil.getLightSkyAlt(singleAdjDataPoint); + else + skyLights.get(direction)[faceToDraw] = skyLights.get(Direction.UP)[0]; + } + faceToDraw++; + } + + adjHeight.get(direction)[faceToDraw] = VOID_FACE; + adjDepth.get(direction)[faceToDraw] = VOID_FACE; + } + } + + /** We use this method to set the various width of the column */ + public void setWidth(int xWidth, int yWidth, int zWidth) + { + boxWidth[X] = xWidth; + boxWidth[Y] = yWidth; + boxWidth[Z] = zWidth; + } + + /** We use this method to set the various offset of the column */ + public void setOffset(int xOffset, int yOffset, int zOffset) + { + boxOffset[X] = xOffset; + boxOffset[Y] = yOffset; + boxOffset[Z] = zOffset; + } + + /** + * This method return the position of a face in the axis of the face + * This is useful for the face culling + * @param direction that we want to check + * @return position in the axis of the face + */ + public int getFacePos(Direction direction) + { + return boxOffset[FACE_DIRECTION.get(direction)[0]] + boxWidth[FACE_DIRECTION.get(direction)[0]] * FACE_DIRECTION.get(direction)[1]; + } + + /** + * returns true if the given direction should be rendered. + */ + public boolean shouldRenderFace(Direction direction, int adjIndex) + { + if (direction == Direction.UP || direction == Direction.DOWN) + return adjIndex == 0; + return !(adjHeight.get(direction)[adjIndex] == VOID_FACE && adjDepth.get(direction)[adjIndex] == VOID_FACE); + } + + + /** + * @param direction direction of the face we want to render + * @param vertexIndex index of the vertex of the face (0-1-2-3) + * @return position x of the relative vertex + */ + public int getX(Direction direction, int vertexIndex) + { + return boxOffset[X] + boxWidth[X] * DIRECTION_VERTEX_MAP.get(direction)[vertexIndex][X]; + } + + /** + * @param direction direction of the face we want to render + * @param vertexIndex index of the vertex of the face (0-1-2-3) + * @return position y of the relative vertex + */ + public int getY(Direction direction, int vertexIndex) + { + return boxOffset[Y] + boxWidth[Y] * DIRECTION_VERTEX_MAP.get(direction)[vertexIndex][Y]; + } + + /** + * @param direction direction of the face we want to render + * @param vertexIndex index of the vertex of the face (0-1-2-3) + * @param adjIndex, index of the n-th culled face of this direction + * @return position x of the relative vertex + */ + public int getY(Direction direction, int vertexIndex, int adjIndex) + { + if (direction == Direction.DOWN || direction == Direction.UP) + return boxOffset[Y] + boxWidth[Y] * DIRECTION_VERTEX_MAP.get(direction)[vertexIndex][Y]; + else + { + // this could probably be cleaned up more, + // but it still works + if (1 - DIRECTION_VERTEX_MAP.get(direction)[vertexIndex][Y] == ADJACENT_HEIGHT_INDEX) + return adjHeight.get(direction)[adjIndex]; + else + return adjDepth.get(direction)[adjIndex]; + } + } + + /** + * @param direction direction of the face we want to render + * @param vertexIndex index of the vertex of the face (0-1-2-3) + * @return position z of the relative vertex + */ + public int getZ(Direction direction, int vertexIndex) + { + return boxOffset[Z] + boxWidth[Z] * DIRECTION_VERTEX_MAP.get(direction)[vertexIndex][Z]; + } + + public int getMinX() + { + return boxOffset[X]; + } + + public int getMaxX() + { + return boxOffset[X] + boxWidth[X]; + } + + public int getMinY() + { + return boxOffset[Y]; + } + + public int getMaxY() + { + return boxOffset[Y] + boxWidth[Y]; + } + + public int getMinZ() + { + return boxOffset[Z]; + } + + public int getMaxZ() + { + return boxOffset[Z] + boxWidth[Z]; + } + +} diff --git a/src/main/java/com/seibel/lod/builders/bufferBuilding/lodTemplates/CubicLodTemplate.java b/src/main/java/com/seibel/lod/builders/bufferBuilding/lodTemplates/CubicLodTemplate.java new file mode 100644 index 000000000..677dc7c7d --- /dev/null +++ b/src/main/java/com/seibel/lod/builders/bufferBuilding/lodTemplates/CubicLodTemplate.java @@ -0,0 +1,141 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.builders.bufferBuilding.lodTemplates; + +import java.util.Map; + +import com.seibel.lod.enums.DebugMode; +import com.seibel.lod.util.ColorUtil; +import com.seibel.lod.util.DataPointUtil; +import com.seibel.lod.util.LodUtil; + +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.util.Direction; +import net.minecraft.util.math.BlockPos; + +/** + * Builds LODs as rectangular prisms. + * @author James Seibel + * @version 10-10-2021 + */ +public class CubicLodTemplate extends AbstractLodTemplate +{ + + public CubicLodTemplate() + { + + } + + @Override + public void addLodToBuffer(BufferBuilder buffer, BlockPos bufferCenterBlockPos, long data, Map adjData, + byte detailLevel, int posX, int posZ, Box box, DebugMode debugging, boolean[] adjShadeDisabled) + { + if (box == null) + return; + + // equivalent to 2^detailLevel + int blockWidth = 1 << detailLevel; + + int color; + if (debugging != DebugMode.OFF) + color = LodUtil.DEBUG_DETAIL_LEVEL_COLORS[detailLevel].getRGB(); + else + color = DataPointUtil.getColor(data); + + + generateBoundingBox( + box, + DataPointUtil.getHeight(data), + DataPointUtil.getDepth(data), + blockWidth, + posX * blockWidth, 0, posZ * blockWidth, // x, y, z offset + bufferCenterBlockPos, + adjData, + color, + DataPointUtil.getLightSkyAlt(data), + DataPointUtil.getLightBlock(data), + adjShadeDisabled); + + addBoundingBoxToBuffer(buffer, box); + } + + private void generateBoundingBox(Box box, + int height, int depth, int width, + double xOffset, double yOffset, double zOffset, + BlockPos bufferCenterBlockPos, + Map adjData, + int color, + int skyLight, + int blockLight, + boolean[] adjShadeDisabled) + { + // don't add an LOD if it is empty + if (height == -1 && depth == -1) + return; + + if (depth == height) + // if the top and bottom points are at the same height + // render this LOD as 1 block thick + height++; + + // offset the AABB by its x/z position in the world since + // it uses doubles to specify its location, unlike the model view matrix + // which only uses floats + double x = -bufferCenterBlockPos.getX(); + double z = -bufferCenterBlockPos.getZ(); + box.reset(); + box.setColor(color, adjShadeDisabled); + box.setLights(skyLight, blockLight); + box.setWidth(width, height - depth, width); + box.setOffset((int) (xOffset + x), (int) (depth + yOffset), (int) (zOffset + z)); + box.setUpCulling(32, bufferCenterBlockPos); + box.setAdjData(adjData); + } + + private void addBoundingBoxToBuffer(BufferBuilder buffer, Box box) + { + int color; + int skyLight; + int blockLight; + for (Direction direction : Box.DIRECTIONS) + { + if(box.isCulled(direction)) + continue; + int verticalFaceIndex = 0; + while (box.shouldRenderFace(direction, verticalFaceIndex)) + { + for (int vertexIndex = 0; vertexIndex < 4; vertexIndex++) + { + color = box.getColor(direction); + skyLight = box.getSkyLight(direction, verticalFaceIndex); + blockLight = box.getBlockLight(); + color = ColorUtil.applyLightValue(color, skyLight, blockLight); + addPosAndColor(buffer, + box.getX(direction, vertexIndex), + box.getY(direction, vertexIndex, verticalFaceIndex), + box.getZ(direction, vertexIndex), + color); + } + verticalFaceIndex++; + } + } + } + +} diff --git a/src/main/java/com/seibel/lod/builders/bufferBuilding/lodTemplates/DynamicLodTemplate.java b/src/main/java/com/seibel/lod/builders/bufferBuilding/lodTemplates/DynamicLodTemplate.java new file mode 100644 index 000000000..dd88744ab --- /dev/null +++ b/src/main/java/com/seibel/lod/builders/bufferBuilding/lodTemplates/DynamicLodTemplate.java @@ -0,0 +1,48 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.builders.bufferBuilding.lodTemplates; + +import java.util.Map; + +import com.seibel.lod.enums.DebugMode; +import com.seibel.lod.proxy.ClientProxy; + +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.util.Direction; +import net.minecraft.util.math.BlockPos; + +/** + * TODO DynamicLodTemplate + * Chunks smoothly transition between + * each other, unless a neighboring chunk + * is at a significantly different height. + * @author James Seibel + * @version 06-16-2021 + */ +public class DynamicLodTemplate extends AbstractLodTemplate +{ + @Override + public void addLodToBuffer(BufferBuilder buffer, BlockPos bufferCenterBlockPos, long data, Map adjData, + byte detailLevel, int posX, int posZ, Box box, DebugMode debugging, boolean[] adjShadeDisabled) + { + ClientProxy.LOGGER.error(DynamicLodTemplate.class.getSimpleName() + " is not implemented!"); + } + +} diff --git a/src/main/java/com/seibel/lod/builders/bufferBuilding/lodTemplates/TriangularLodTemplate.java b/src/main/java/com/seibel/lod/builders/bufferBuilding/lodTemplates/TriangularLodTemplate.java new file mode 100644 index 000000000..765399455 --- /dev/null +++ b/src/main/java/com/seibel/lod/builders/bufferBuilding/lodTemplates/TriangularLodTemplate.java @@ -0,0 +1,46 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.builders.bufferBuilding.lodTemplates; + +import java.util.Map; + +import com.seibel.lod.enums.DebugMode; +import com.seibel.lod.proxy.ClientProxy; + +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.util.Direction; +import net.minecraft.util.math.BlockPos; + +/** + * TODO #21 TriangularLodTemplate + * Builds each LOD chunk as a singular rectangular prism. + * @author James Seibel + * @version 06-16-2021 + */ +public class TriangularLodTemplate extends AbstractLodTemplate +{ + @Override + public void addLodToBuffer(BufferBuilder buffer, BlockPos bufferCenterBlockPos, long data, Map adjData, + byte detailLevel, int posX, int posZ, Box box, DebugMode debugging, boolean[] adjShadeDisabled) + { + ClientProxy.LOGGER.error(DynamicLodTemplate.class.getSimpleName() + " is not implemented!"); + } + +} diff --git a/src/main/java/com/seibel/lod/builders/lodBuilding/LodBuilder.java b/src/main/java/com/seibel/lod/builders/lodBuilding/LodBuilder.java new file mode 100644 index 000000000..c11a7ed2a --- /dev/null +++ b/src/main/java/com/seibel/lod/builders/lodBuilding/LodBuilder.java @@ -0,0 +1,543 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.builders.lodBuilding; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.seibel.lod.config.LodConfig; +import com.seibel.lod.enums.DistanceGenerationMode; +import com.seibel.lod.enums.HorizontalResolution; +import com.seibel.lod.objects.LodDimension; +import com.seibel.lod.objects.LodRegion; +import com.seibel.lod.objects.LodWorld; +import com.seibel.lod.util.ColorUtil; +import com.seibel.lod.util.DataPointUtil; +import com.seibel.lod.util.DetailDistanceUtil; +import com.seibel.lod.util.LevelPosUtil; +import com.seibel.lod.util.LodThreadFactory; +import com.seibel.lod.util.LodUtil; +import com.seibel.lod.util.ThreadMapUtil; +import com.seibel.lod.wrappers.MinecraftWrapper; +import com.seibel.lod.wrappers.Block.BlockColorWrapper; +import com.seibel.lod.wrappers.Block.BlockPosWrapper; +import com.seibel.lod.wrappers.Block.BlockShapeWrapper; +import com.seibel.lod.wrappers.Chunk.ChunkPosWrapper; +import com.seibel.lod.wrappers.Chunk.ChunkWrapper; +import com.seibel.lod.wrappers.World.BiomeColorWrapper; +import com.seibel.lod.wrappers.World.BiomeWrapper; +import com.seibel.lod.wrappers.World.WorldWrapper; + +import net.minecraft.world.DimensionType; +import net.minecraft.world.IWorld; + +/** + * This object is in charge of creating Lod related objects. + * + * @author Cola + * @author Leonardo Amato + * @author James Seibel + * @version 10-22-2021 + */ +public class LodBuilder +{ + private static final MinecraftWrapper mc = MinecraftWrapper.INSTANCE; + + private final ExecutorService lodGenThreadPool = Executors.newSingleThreadExecutor(new LodThreadFactory(this.getClass().getSimpleName())); + + + /** If no blocks are found in the area in determineBottomPointForArea return this */ + public static final short DEFAULT_DEPTH = 0; + /** If no blocks are found in the area in determineHeightPointForArea return this */ + public static final short DEFAULT_HEIGHT = 0; + /** Minecraft's max light value */ + public static final short DEFAULT_MAX_LIGHT = 15; + + + /** + * How wide LodDimensions should be in regions
+ * Is automatically set before the first frame in ClientProxy. + */ + public int defaultDimensionWidthInRegions = 0; + + //public static final boolean useExperimentalLighting = true; + + + + + public LodBuilder() + { + + } + + public void generateLodNodeAsync(ChunkWrapper chunk, LodWorld lodWorld, IWorld world) + { + generateLodNodeAsync(chunk, lodWorld, world, DistanceGenerationMode.SERVER); + } + + public void generateLodNodeAsync(ChunkWrapper chunk, LodWorld lodWorld, IWorld world, DistanceGenerationMode generationMode) + { + if (lodWorld == null || lodWorld.getIsWorldNotLoaded()) + return; + + // don't try to create an LOD object + // if for some reason we aren't + // given a valid chunk object + if (chunk == null) + return; + + Thread thread = new Thread(() -> + { + try + { + // we need a loaded client world in order to + // get the textures for blocks + if (mc.getClientWorld() == null) + return; + + // don't try to generate LODs if the user isn't in the world anymore + // (this happens a lot when the user leaves a world/server) + if (mc.getSinglePlayerServer() == null && mc.getCurrentServer() == null) + return; + + DimensionType dim = world.dimensionType(); + + // make sure the dimension exists + LodDimension lodDim; + if (lodWorld.getLodDimension(dim) == null) + { + lodDim = new LodDimension(dim, lodWorld, defaultDimensionWidthInRegions); + lodWorld.addLodDimension(lodDim); + } + else + { + lodDim = lodWorld.getLodDimension(dim); + } + generateLodNodeFromChunk(lodDim, chunk, new LodBuilderConfig(generationMode)); + } + catch (IllegalArgumentException | NullPointerException e) + { + e.printStackTrace(); + // if the world changes while LODs are being generated + // they will throw errors as they try to access things that no longer + // exist. + } + }); + lodGenThreadPool.execute(thread); + } + + /** + * Creates a LodNode for a chunk in the given world. + * @throws IllegalArgumentException thrown if either the chunk or world is null. + */ + public void generateLodNodeFromChunk(LodDimension lodDim, ChunkWrapper chunk) throws IllegalArgumentException + { + generateLodNodeFromChunk(lodDim, chunk, new LodBuilderConfig()); + } + + /** + * Creates a LodNode for a chunk in the given world. + * @throws IllegalArgumentException thrown if either the chunk or world is null. + */ + public void generateLodNodeFromChunk(LodDimension lodDim, ChunkWrapper chunk, LodBuilderConfig config) + throws IllegalArgumentException + { + if (chunk == null) + throw new IllegalArgumentException("generateLodFromChunk given a null chunk"); + + int startX; + int startZ; + + + LodRegion region = lodDim.getRegion(chunk.getPos().getRegionX(), chunk.getPos().getRegionZ()); + if (region == null) + return; + + // this happens if a LOD is generated after the user leaves the world. + if (MinecraftWrapper.INSTANCE.getWrappedClientWorld() == null) + return; + + // determine how many LODs to generate horizontally + byte minDetailLevel = region.getMinDetailLevel(); + HorizontalResolution detail = DetailDistanceUtil.getLodGenDetail(minDetailLevel); + + + // determine how many LODs to generate vertically + //VerticalQuality verticalQuality = LodConfig.CLIENT.graphics.qualityOption.verticalQuality.get(); + byte detailLevel = detail.detailLevel; + + + // generate the LODs + int posX; + int posZ; + for (int i = 0; i < detail.dataPointLengthCount * detail.dataPointLengthCount; i++) + { + startX = detail.startX[i]; + startZ = detail.startZ[i]; + + long[] data; + long[] dataToMergeVertical = createVerticalDataToMerge(detail, chunk, config, startX, startZ); + data = DataPointUtil.mergeMultiData(dataToMergeVertical, DataPointUtil.worldHeight / 2 + 1, DetailDistanceUtil.getMaxVerticalData(detailLevel)); + + + //lodDim.clear(detailLevel, posX, posZ); + if (data != null && data.length != 0) + { + posX = LevelPosUtil.convert((byte) 0, chunk.getPos().getX() * 16 + startX, detail.detailLevel); + posZ = LevelPosUtil.convert((byte) 0, chunk.getPos().getZ() * 16 + startZ, detail.detailLevel); + lodDim.addVerticalData(detailLevel, posX, posZ, data, false); + } + } + lodDim.updateData(LodUtil.CHUNK_DETAIL_LEVEL, chunk.getPos().getX(), chunk.getPos().getZ()); + } + + /** creates a vertical DataPoint */ + private long[] createVerticalDataToMerge(HorizontalResolution detail, ChunkWrapper chunk, LodBuilderConfig config, int startX, int startZ) + { + // equivalent to 2^detailLevel + int size = 1 << detail.detailLevel; + + long[] dataToMerge = ThreadMapUtil.getBuilderVerticalArray(detail.detailLevel); + int verticalData = DataPointUtil.worldHeight / 2 + 1; + + ChunkPosWrapper chunkPos = chunk.getPos(); + int height; + int depth; + int color; + int light; + int lightSky; + int lightBlock; + int generation = config.distanceGenerationMode.complexity; + + int xRel; + int zRel; + int xAbs; + int yAbs; + int zAbs; + boolean hasCeiling = mc.getClientWorld().dimensionType().hasCeiling(); + boolean hasSkyLight = mc.getClientWorld().dimensionType().hasSkyLight(); + boolean isDefault; + BlockPosWrapper blockPos = new BlockPosWrapper(); + int index; + + for (index = 0; index < size * size; index++) + { + xRel = startX + index % size; + zRel = startZ + index / size; + xAbs = chunkPos.getMinBlockX() + xRel; + zAbs = chunkPos.getMinBlockZ() + zRel; + + //Calculate the height of the lod + yAbs = DataPointUtil.worldHeight + 1; + int count = 0; + boolean topBlock = true; + while (yAbs > 0) + { + height = determineHeightPointFrom(chunk, config, xRel, yAbs, zRel, blockPos); + + // If the lod is at the default height, it must be void data + if (height == DEFAULT_HEIGHT) + { + if (topBlock) + dataToMerge[index * verticalData] = DataPointUtil.createVoidDataPoint(generation); + break; + } + + yAbs = height - 1; + // We search light on above air block + depth = determineBottomPointFrom(chunk, config, xRel, yAbs, zRel, blockPos); + if (hasCeiling && topBlock) + { + yAbs = depth; + blockPos.set(xAbs, yAbs, zAbs); + light = getLightValue(chunk, blockPos, true, hasSkyLight, true); + color = generateLodColor(chunk, config, xAbs, yAbs, zAbs, blockPos); + blockPos.set(xAbs, yAbs - 1, zAbs); + } + else + { + blockPos.set(xAbs, yAbs, zAbs); + light = getLightValue(chunk, blockPos, hasCeiling, hasSkyLight, topBlock); + color = generateLodColor(chunk, config, xRel, yAbs, zRel, blockPos); + blockPos.set(xAbs, yAbs + 1, zAbs); + } + lightBlock = light & 0b1111; + lightSky = (light >> 4) & 0b1111; + isDefault = ((light >> 8)) == 1; + + dataToMerge[index * verticalData + count] = DataPointUtil.createDataPoint(height, depth, color, lightSky, lightBlock, generation, isDefault); + topBlock = false; + yAbs = depth - 1; + count++; + } + } + return dataToMerge; + } + + /** + * Find the lowest valid point from the bottom. + * Used when creating a vertical LOD. + */ + private short determineBottomPointFrom(ChunkWrapper chunk, LodBuilderConfig config, int xAbs, int yAbs, int zAbs, BlockPosWrapper blockPos) + { + short depth = DEFAULT_DEPTH; + + for (int y = yAbs; y >= 0; y--) + { + blockPos.set(xAbs, y, zAbs); + if (!isLayerValidLodPoint(chunk, blockPos)) + { + depth = (short) (y + 1); + break; + } + } + return depth; + } + + /** Find the highest valid point from the Top */ + private short determineHeightPointFrom(ChunkWrapper chunk, LodBuilderConfig config, int xAbs, int yAbs, int zAbs, BlockPosWrapper blockPos) + { + short height = DEFAULT_HEIGHT; + if (config.useHeightmap) + height = (short) chunk.getHeightMapValue(xAbs, zAbs); + else + { + for (int y = yAbs; y >= 0; y--) + { + blockPos.set(xAbs, y, zAbs); + if (isLayerValidLodPoint(chunk, blockPos)) + { + height = (short) (y + 1); + break; + } + } + } + return height; + } + + + + // =====================// + // constructor helpers // + // =====================// + + /** + * Generate the color for the given chunk using biome water color, foliage + * color, and grass color. + */ + private int generateLodColor(ChunkWrapper chunk, LodBuilderConfig config, int xRel, int yAbs, int zRel, BlockPosWrapper blockPos) + { + int colorInt; + if (config.useBiomeColors) + { + // I have no idea why I need to bit shift to the right, but + // if I don't the biomes don't show up correctly. + colorInt = chunk.getBiome(xRel, yAbs, zRel).getColorForBiome(xRel, zRel); + } + else + { + blockPos.set(chunk.getPos().getMinBlockX() + xRel, yAbs, chunk.getPos().getMinBlockZ() + zRel); + colorInt = getColorForBlock(chunk, blockPos); + + // if we are skipping non-full and non-solid blocks that means we ignore + // snow, flowers, etc. Get the above block so we can still get the color + // of the snow, flower, etc. that may be above this block + int aboveColorInt = 0; + if (LodConfig.CLIENT.worldGenerator.blockToAvoid.get().nonFull || LodConfig.CLIENT.worldGenerator.blockToAvoid.get().noCollision) + { + blockPos.set(chunk.getPos().getMinBlockX() + xRel, yAbs + 1, chunk.getPos().getMinBlockZ() + zRel); + aboveColorInt = getColorForBlock(chunk, blockPos); + } + + //if (colorInt == 0 && yAbs > 0) + // if this block is invisible, check the block below it + // colorInt = generateLodColor(chunk, config, xRel, yAbs - 1, zRel, blockPos); + + // override this block's color if there was a block above this + // and we were avoiding non-full/non-solid blocks + if (aboveColorInt != 0) + colorInt = aboveColorInt; + } + + return colorInt; + } + + /** Gets the light value for the given block position */ + private int getLightValue(ChunkWrapper chunk, BlockPosWrapper blockPos, boolean hasCeiling, boolean hasSkyLight, boolean topBlock) + { + int skyLight = 0; + int blockLight; + // 1 means the lighting is a guess + int isDefault = 0; + + WorldWrapper world = MinecraftWrapper.INSTANCE.getWrappedServerWorld(); + + int blockBrightness = chunk.getEmittedBrightness(blockPos); + // get the air block above or below this block + if (hasCeiling && topBlock) + blockPos.set(blockPos.getX(), blockPos.getY() - 1, blockPos.getZ()); + else + blockPos.set(blockPos.getX(), blockPos.getY() + 1, blockPos.getZ()); + + + + if (world != null && !world.isEmpty()) + { + // server world sky light (always accurate) + blockLight = world.getBlockLight(blockPos); + if (topBlock && !hasCeiling && hasSkyLight) + skyLight = DEFAULT_MAX_LIGHT; + else + { + if (hasSkyLight) + skyLight = world.getSkyLight(blockPos); + //else + // skyLight = 0; + } + if (!topBlock && skyLight == 15) + { + // we are on predicted terrain, and we don't know what the light here is, + // lets just take a guess + if (blockPos.getY() >= mc.getClientWorld().getSeaLevel() - 5) + { + skyLight = 12; + isDefault = 1; + } + else + skyLight = 0; + } + } + else + { + world = MinecraftWrapper.INSTANCE.getWrappedClientWorld(); + if (world.isEmpty()) + return 0; + // client world sky light (almost never accurate) + blockLight = world.getBlockLight(blockPos); + // estimate what the lighting should be + if (hasSkyLight || !hasCeiling) + { + if (topBlock) + skyLight = DEFAULT_MAX_LIGHT; + else + { + + if (hasSkyLight) + skyLight = world.getSkyLight(blockPos); + //else + // skyLight = 0; + + if (!chunk.isLightCorrect() && (skyLight == 0 || skyLight == 15)) + { + // we don't know what the light here is, + // lets just take a guess + if (blockPos.getY() >= mc.getClientWorld().getSeaLevel() - 5) + { + skyLight = 12; + isDefault = 1; + } + else + skyLight = 0; + } + } + if (hasSkyLight) + skyLight = 0; + } + } + + blockLight = LodUtil.clamp(0, Math.max(blockLight, blockBrightness), DEFAULT_MAX_LIGHT); + + return blockLight + (skyLight << 4) + (isDefault << 8); + } + + /** Returns a color int for the given block. */ + private int getColorForBlock(ChunkWrapper chunk, BlockPosWrapper blockPos) + { + + + int colorOfBlock; + int colorInt; + + int xRel = blockPos.getX() - chunk.getPos().getMinBlockX(); + int zRel = blockPos.getZ() - chunk.getPos().getMinBlockZ(); + //int x = blockPos.getX(); + int y = blockPos.getY(); + //int z = blockPos.getZ(); + + BlockColorWrapper blockColorWrapper; + BlockShapeWrapper blockShapeWrapper = chunk.getBlockShapeWrapper(blockPos); + + if (chunk.isWaterLogged(blockPos)) + { + BiomeWrapper biome = chunk.getBiome(xRel, y, zRel); + return biome.getWaterTint(); + } + else + blockColorWrapper = chunk.getBlockColorWrapper(blockPos); + + if (blockShapeWrapper.isToAvoid()) + return 0; + + colorOfBlock = blockColorWrapper.getColor(); + + + if (blockColorWrapper.hasTint()) + { + WorldWrapper world = MinecraftWrapper.INSTANCE.getWrappedServerWorld(); + + if (world == null || world.isEmpty()) + world = MinecraftWrapper.INSTANCE.getWrappedClientWorld(); + + int tintValue; + if (blockColorWrapper.hasGrassTint()) + // grass and green plants + tintValue = BiomeColorWrapper.getGrassColor(world, blockPos); + else if (blockColorWrapper.hasFolliageTint()) + tintValue = BiomeColorWrapper.getFoliageColor(world, blockPos); + else + //we can reintroduce this with the wrappers + tintValue = BiomeColorWrapper.getWaterColor(world, blockPos); + + colorInt = ColorUtil.multiplyRGBcolors(tintValue | 0xFF000000, colorOfBlock); + } + else + colorInt = colorOfBlock; + return colorInt; + } + + + /** Is the block at the given blockPos a valid LOD point? */ + private boolean isLayerValidLodPoint(ChunkWrapper chunk, BlockPosWrapper blockPos) + { + + + if (chunk.isWaterLogged(blockPos)) + return true; + + boolean nonFullAvoidance = LodConfig.CLIENT.worldGenerator.blockToAvoid.get().nonFull; + boolean noCollisionAvoidance = LodConfig.CLIENT.worldGenerator.blockToAvoid.get().noCollision; + + BlockShapeWrapper block = chunk.getBlockShapeWrapper(blockPos); + return !block.isToAvoid() + && !(nonFullAvoidance && block.isNonFull()) + && !(noCollisionAvoidance && block.hasNoCollision()); + + } +} diff --git a/src/main/java/com/seibel/lod/builders/lodBuilding/LodBuilderConfig.java b/src/main/java/com/seibel/lod/builders/lodBuilding/LodBuilderConfig.java new file mode 100644 index 000000000..89569f486 --- /dev/null +++ b/src/main/java/com/seibel/lod/builders/lodBuilding/LodBuilderConfig.java @@ -0,0 +1,95 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.builders.lodBuilding; + +import com.seibel.lod.enums.DistanceGenerationMode; + +/** + * This is used to easily configure how LodChunks are generated. + * Generally this will only be used if we want to generate a + * LodChunk using an incomplete Chunk, otherwise the defaults + * work best for a fully generated chunk (IE has correct surface blocks). + * @author James Seibel + * @version 8-14-2021 + */ +public class LodBuilderConfig +{ + /** default: false */ + public boolean useHeightmap; + /** default: false */ + public boolean useBiomeColors; + /** default: true */ + public boolean useSolidBlocksInColorGen; + /** default: server */ + public DistanceGenerationMode distanceGenerationMode; + + /** + * default settings for a normal chunk
+ * useHeightmap = false
+ * useBiomeColors = false
+ * useSolidBlocksInColorGen = true
+ * generationMode = Server
+ */ + public LodBuilderConfig() + { + useHeightmap = false; + useBiomeColors = false; + useSolidBlocksInColorGen = true; + distanceGenerationMode = DistanceGenerationMode.SERVER; + } + + /** + * @param newUseHeightmap default = false + * @param newUseBiomeColors default = false + * @param newUseSolidBlocksInBiomeColor default = true + * @param newDistanceGenerationMode default = Server + */ + public LodBuilderConfig(boolean newUseHeightmap, boolean newUseBiomeColors, + boolean newUseSolidBlocksInBiomeColor, DistanceGenerationMode newDistanceGenerationMode) + { + useHeightmap = newUseHeightmap; + useBiomeColors = newUseBiomeColors; + useSolidBlocksInColorGen = newUseSolidBlocksInBiomeColor; + distanceGenerationMode = newDistanceGenerationMode; + } + + /** + * @param newUseHeightmap default = false + * @param newUseBiomeColors default = false + * @param newUseSolidBlocksInBiomeColor default = true + */ + public LodBuilderConfig(boolean newUseHeightmap, boolean newUseBiomeColors, boolean newUseSolidBlocksInBiomeColor) + { + this(); + useHeightmap = newUseHeightmap; + useBiomeColors = newUseBiomeColors; + useSolidBlocksInColorGen = newUseSolidBlocksInBiomeColor; + distanceGenerationMode = newUseHeightmap ? DistanceGenerationMode.BIOME_ONLY_SIMULATE_HEIGHT : DistanceGenerationMode.BIOME_ONLY; + } + + /** + * @param newDistanceGenerationMode default = Server + */ + public LodBuilderConfig(DistanceGenerationMode newDistanceGenerationMode) + { + this(); + distanceGenerationMode = newDistanceGenerationMode; + } +} \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/builders/worldGeneration/LodGenWorker.java b/src/main/java/com/seibel/lod/builders/worldGeneration/LodGenWorker.java new file mode 100644 index 000000000..4fed6fb9b --- /dev/null +++ b/src/main/java/com/seibel/lod/builders/worldGeneration/LodGenWorker.java @@ -0,0 +1,705 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.builders.worldGeneration; + +import java.util.ConcurrentModificationException; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.seibel.lod.builders.lodBuilding.LodBuilder; +import com.seibel.lod.builders.lodBuilding.LodBuilderConfig; +import com.seibel.lod.config.LodConfig; +import com.seibel.lod.enums.DistanceGenerationMode; +import com.seibel.lod.objects.LodDimension; +import com.seibel.lod.proxy.ClientProxy; +import com.seibel.lod.util.LodUtil; + +import com.seibel.lod.wrappers.Chunk.ChunkWrapper; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.util.palette.UpgradeData; +import net.minecraft.util.registry.Registry; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.chunk.ChunkPrimer; +import net.minecraft.world.chunk.ChunkStatus; +import net.minecraft.world.chunk.IChunk; +import net.minecraft.world.gen.ChunkGenerator; +import net.minecraft.world.gen.Heightmap; +import net.minecraft.world.gen.blockstateprovider.WeightedBlockStateProvider; +import net.minecraft.world.gen.feature.BlockClusterFeatureConfig; +import net.minecraft.world.gen.feature.ConfiguredFeature; +import net.minecraft.world.gen.feature.DecoratedFeatureConfig; +import net.minecraft.world.gen.feature.FeatureSpread; +import net.minecraft.world.gen.feature.FeatureSpreadConfig; +import net.minecraft.world.gen.feature.IceAndSnowFeature; +import net.minecraft.world.gen.feature.NoFeatureConfig; +import net.minecraft.world.gen.feature.template.TemplateManager; +import net.minecraft.world.gen.placement.ConfiguredPlacement; +import net.minecraft.world.gen.placement.DecoratedPlacementConfig; +import net.minecraft.world.gen.placement.IPlacementConfig; +import net.minecraft.world.gen.placement.NoiseDependant; +import net.minecraft.world.server.ServerChunkProvider; +import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.server.ServerWorldLightManager; +import net.minecraftforge.common.WorldWorkerManager.IWorker; + +/** + * This is used to generate a LodChunk at a given ChunkPos. + * + * @author James Seibel + * @version 10-22-2021 + */ +public class LodGenWorker implements IWorker +{ + public static ExecutorService genThreads = Executors.newFixedThreadPool(LodConfig.CLIENT.advancedModOptions.threading.numberOfWorldGenerationThreads.get(), new ThreadFactoryBuilder().setNameFormat("Gen-Worker-Thread-%d").build()); + + private boolean threadStarted = false; + private final LodChunkGenThread thread; + + + /** + * If a configured feature fails for whatever reason, + * add it to this list, this is to hopefully remove any + * features that could cause issues down the line. + */ + private static final ConcurrentHashMap> configuredFeaturesToAvoid = new ConcurrentHashMap<>(); + + + + public LodGenWorker(ChunkPos newPos, DistanceGenerationMode newGenerationMode, + LodBuilder newLodBuilder, + LodDimension newLodDimension, ServerWorld newServerWorld) + { + // just a few sanity checks + if (newPos == null) + throw new IllegalArgumentException("LodChunkGenWorker must have a non-null ChunkPos"); + + if (newLodBuilder == null) + throw new IllegalArgumentException("LodChunkGenThread requires a non-null LodChunkBuilder"); + + if (newLodDimension == null) + throw new IllegalArgumentException("LodChunkGenThread requires a non-null LodDimension"); + + if (newServerWorld == null) + throw new IllegalArgumentException("LodChunkGenThread requires a non-null ServerWorld"); + + + + thread = new LodChunkGenThread(newPos, newGenerationMode, + newLodBuilder, + newLodDimension, newServerWorld); + } + + @Override + public boolean doWork() + { + if (!threadStarted) + { + if (LodConfig.CLIENT.worldGenerator.distanceGenerationMode.get() == DistanceGenerationMode.SERVER) + { + // if we are using SERVER generation that has to be done + // synchronously to prevent crashing and harmful + // interactions with the normal world generator + thread.run(); + } + else + { + // Every other method can + // be done asynchronously + Thread newThread = new Thread(thread); + newThread.setPriority(5); + genThreads.execute(newThread); + } + + threadStarted = true; + + // useful for debugging +// ClientProxy.LOGGER.info(thread.lodDim.getNumberOfLods()); +// ClientProxy.LOGGER.info(genThreads.toString()); + } + + return false; + } + + @Override + public boolean hasWork() + { + return !threadStarted; + } + + + + + private static class LodChunkGenThread implements Runnable + { + public final ServerWorld serverWorld; + public final LodDimension lodDim; + public final DistanceGenerationMode generationMode; + public final LodBuilder lodBuilder; + + private final ChunkPos pos; + + public LodChunkGenThread(ChunkPos newPos, DistanceGenerationMode newGenerationMode, + LodBuilder newLodBuilder, + LodDimension newLodDimension, ServerWorld newServerWorld) + { + pos = newPos; + generationMode = newGenerationMode; + lodBuilder = newLodBuilder; + lodDim = newLodDimension; + serverWorld = newServerWorld; + } + + @Override + public void run() + { + try + { + // only generate LodChunks if they can + // be added to the current LodDimension + + /* TODO I must disable this 'if', if I will find a way to replace it */ + if (lodDim.regionIsInRange(pos.x / LodUtil.REGION_WIDTH_IN_CHUNKS, pos.z / LodUtil.REGION_WIDTH_IN_CHUNKS)) + { + // + //{ + // lodBuilder.generateLodNodeFromChunk(lodDim, loadedChunk, new LodBuilderConfig(DistanceGenerationMode.SERVER)); + //} + //else + //{ + /* + IChunk loadedChunk = null; + if (lodDim.isChunkPreGenerated(pos.x, pos.z) && LodConfig.CLIENT.worldGenerator.useExperimentalPreGenLoading.get()) + { + // generate a Lod like normal + loadedChunk = ChunkLoader.getChunkFromFile(pos); + if(loadedChunk != null) + lodBuilder.generateLodNodeFromChunk(lodDim, loadedChunk, new LodBuilderConfig(DistanceGenerationMode.SERVER)); + else + { + switch (generationMode) + { + case NONE: + // don't generate + break; + case BIOME_ONLY: + case BIOME_ONLY_SIMULATE_HEIGHT: + // fastest + generateUsingBiomesOnly(); + break; + case SURFACE: + // faster + generateUsingSurface(); + break; + case FEATURES: + // fast + generateUsingFeatures(); + break; + case SERVER: + // very slow + lodBuilder.generateLodNodeFromChunk(lodDim, serverWorld.getChunk(pos.x, pos.z, ChunkStatus.FEATURES), new LodBuilderConfig(DistanceGenerationMode.SERVER)); + //generateWithServer(); + break; + } + } + } + else + {*/ + switch (generationMode) + { + case NONE: + // don't generate + break; + case BIOME_ONLY: + case BIOME_ONLY_SIMULATE_HEIGHT: + // fastest + generateUsingBiomesOnly(); + break; + case SURFACE: + // faster + generateUsingSurface(); + break; + case FEATURES: + // fast + generateUsingFeatures(); + break; + case SERVER: + // very slow + generateWithServer(); + break; + } + //} + + //lodRenderer.regenerateLODsNextFrame(); + +/* + boolean dataExistence = lodDim.doesDataExist(new LevelPos((byte) 3, pos.x, pos.z)); + if (dataExistence) + ClientProxy.LOGGER.info(pos.x + " " + pos.z + " Success!"); + else + ClientProxy.LOGGER.info(pos.x + " " + pos.z); + */ + // shows the pool size, active threads, queued tasks and completed tasks +// ClientProxy.LOGGER.info(genThreads.toString()); + +// long endTime = System.currentTimeMillis(); +// System.out.println(endTime - startTime); + + }// if in range + } + catch (Exception e) + { + ClientProxy.LOGGER.error(LodChunkGenThread.class.getSimpleName() + ": ran into an error: " + e.getMessage()); + e.printStackTrace(); + } + finally + { + // decrement how many threads are running + LodWorldGenerator.INSTANCE.numberOfChunksWaitingToGenerate.addAndGet(-1); + + // this position is no longer being generated + LodWorldGenerator.INSTANCE.positionsWaitingToBeGenerated.remove(pos); + } + + }// run + + + + /** + * takes about 2-5 ms + */ + private void generateUsingBiomesOnly() + { + List chunkList = new LinkedList<>(); + ChunkPrimer chunk = new ChunkPrimer(pos, UpgradeData.EMPTY); + chunkList.add(chunk); + + ServerChunkProvider chunkSource = serverWorld.getChunkSource(); + ChunkGenerator chunkGen = chunkSource.generator; + + // generate the terrain (this is thread safe) + ChunkStatus.EMPTY.generate(serverWorld, chunkGen, serverWorld.getStructureManager(), (ServerWorldLightManager) serverWorld.getLightEngine(), null, chunkList); + // override the chunk status, so we can run the next generator stage + chunk.setStatus(ChunkStatus.STRUCTURE_REFERENCES); + chunkGen.createBiomes(serverWorld.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY), chunk); + chunk.setStatus(ChunkStatus.STRUCTURE_REFERENCES); + + + + + // generate fake height data for this LOD + int seaLevel = serverWorld.getSeaLevel(); + + boolean simulateHeight = generationMode == DistanceGenerationMode.BIOME_ONLY_SIMULATE_HEIGHT; + boolean inTheEnd = false; + + // add fake heightmap data so our LODs aren't at height 0 + Heightmap heightmap = new Heightmap(chunk, LodUtil.DEFAULT_HEIGHTMAP); + for (int x = 0; x < LodUtil.CHUNK_WIDTH && !inTheEnd; x++) + { + for (int z = 0; z < LodUtil.CHUNK_WIDTH && !inTheEnd; z++) + { + if (simulateHeight) + { + // these heights are of course aren't super accurate, + // they are just to simulate height data where there isn't any + switch (chunk.getBiomes().getNoiseBiome(x >> 2, seaLevel >> 2, z >> 2).getBiomeCategory()) + { + case NETHER: + heightmap.setHeight(x, z, serverWorld.getHeight() / 2); + break; + + case EXTREME_HILLS: + heightmap.setHeight(x, z, seaLevel + 30); + break; + case MESA: + case JUNGLE: + heightmap.setHeight(x, z, seaLevel + 20); + break; + case BEACH: + heightmap.setHeight(x, z, seaLevel + 5); + break; + case NONE: + heightmap.setHeight(x, z, 0); + break; + + case OCEAN: + case RIVER: + heightmap.setHeight(x, z, seaLevel); + break; + + case THEEND: + inTheEnd = true; + break; + + // DESERT + // FOREST + // ICY + // MUSHROOM + // SAVANNA + // SWAMP + // TAIGA + // PLAINS + default: + heightmap.setHeight(x, z, seaLevel + 10); + break; + }// heightmap switch + } + else + { + // we aren't simulating height + // always use sea level + heightmap.setHeight(x, z, seaLevel); + } + }// z + }// x + + chunk.setHeightmap(LodUtil.DEFAULT_HEIGHTMAP, heightmap.getRawData()); + + + if (!inTheEnd) + { + lodBuilder.generateLodNodeFromChunk(lodDim, new ChunkWrapper(chunk), new LodBuilderConfig(true, true, false)); + } + else + { + // if we are in the end, don't generate any chunks. + // Since we don't know where the islands are, everything + // generates the same, and it looks awful. + //TODO it appears that 'if' can be collapsed, but comment says that it should not be a case + lodBuilder.generateLodNodeFromChunk(lodDim, new ChunkWrapper(chunk), new LodBuilderConfig(true, true, false)); + } + + +// long startTime = System.currentTimeMillis(); +// long endTime = System.currentTimeMillis(); +// System.out.println(endTime - startTime); + } + + + /** + * takes about 10 - 20 ms + */ + private void generateUsingSurface() + { + List chunkList = new LinkedList<>(); + ChunkPrimer chunk = new ChunkPrimer(pos, UpgradeData.EMPTY); + chunkList.add(chunk); + LodServerWorld lodServerWorld = new LodServerWorld(serverWorld, chunk); + + ServerChunkProvider chunkSource = serverWorld.getChunkSource(); + ServerWorldLightManager lightEngine = (ServerWorldLightManager) serverWorld.getLightEngine(); + TemplateManager templateManager = serverWorld.getStructureManager(); + ChunkGenerator chunkGen = chunkSource.generator; + + + // generate the terrain (this is thread safe) + ChunkStatus.EMPTY.generate(serverWorld, chunkGen, templateManager, lightEngine, null, chunkList); + // override the chunk status, so we can run the next generator stage + chunk.setStatus(ChunkStatus.STRUCTURE_REFERENCES); + chunkGen.createBiomes(serverWorld.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY), chunk); + ChunkStatus.NOISE.generate(serverWorld, chunkGen, templateManager, lightEngine, null, chunkList); + ChunkStatus.SURFACE.generate(serverWorld, chunkGen, templateManager, lightEngine, null, chunkList); + + // this feature has been proven to be thread safe, + // so we will add it + IceAndSnowFeature snowFeature = new IceAndSnowFeature(NoFeatureConfig.CODEC); + snowFeature.place(lodServerWorld, chunkGen, serverWorld.random, chunk.getPos().getWorldPosition(), null); + + + lodBuilder.generateLodNodeFromChunk(lodDim, new ChunkWrapper(chunk), new LodBuilderConfig(DistanceGenerationMode.SURFACE)); + + /*TODO if we want to use Biome utils and terrain utils for overworld + * lodBuilder.generateLodNodeFromChunk(lodDim, pos ,detailLevel, serverWorld.getSeed());*/ + } + + + /** + * takes about 15 - 20 ms + *

+ * Causes concurrentModification Exceptions, + * which could cause instability or world generation bugs + */ + private void generateUsingFeatures() + { + List chunkList = new LinkedList<>(); + ChunkPrimer chunk = new ChunkPrimer(pos, UpgradeData.EMPTY); + chunkList.add(chunk); + LodServerWorld lodServerWorld = new LodServerWorld(serverWorld, chunk); + + ServerChunkProvider chunkSource = serverWorld.getChunkSource(); + ServerWorldLightManager lightEngine = (ServerWorldLightManager) serverWorld.getLightEngine(); + TemplateManager templateManager = serverWorld.getStructureManager(); + ChunkGenerator chunkGen = chunkSource.generator; + + + // generate the terrain (this is thread safe) + ChunkStatus.EMPTY.generate(serverWorld, chunkGen, templateManager, lightEngine, null, chunkList); + // override the chunk status, so we can run the next generator stage + chunk.setStatus(ChunkStatus.STRUCTURE_REFERENCES); + chunkGen.createBiomes(serverWorld.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY), chunk); + ChunkStatus.NOISE.generate(serverWorld, chunkGen, templateManager, lightEngine, null, chunkList); + ChunkStatus.SURFACE.generate(serverWorld, chunkGen, templateManager, lightEngine, null, chunkList); + + + // get all the biomes in the chunk + HashSet biomes = new HashSet<>(); + for (int x = 0; x < LodUtil.CHUNK_WIDTH; x++) + { + for (int z = 0; z < LodUtil.CHUNK_WIDTH; z++) + { + Biome biome = chunk.getBiomes().getNoiseBiome(x >> 2, serverWorld.getSeaLevel() >> 2, z >> 2); + + // Issue #35 + // For some reason Jungle biomes cause incredible lag + // the features here must be interacting with each other + // in unpredictable ways (specifically tree feature generation). + // When generating Features my CPU usage generally hovers around 30 - 40% + // when generating Jungles it spikes to 100%. + if (biome.getBiomeCategory() != Biome.Category.JUNGLE) + { + // should probably use the heightmap here instead of seaLevel, + // but this seems to get the job done well enough + biomes.add(biome); + } + } + } + + boolean allowUnstableFeatures = LodConfig.CLIENT.worldGenerator.allowUnstableFeatureGeneration.get(); + + // generate all the features related to this chunk. + // this may or may not be thread safe + for (Biome biome : biomes) + { + List>>> featuresForState = biome.generationSettings.features(); + + for (List>> suppliers : featuresForState) + { + for (Supplier> featureSupplier : suppliers) + { + ConfiguredFeature configuredFeature = featureSupplier.get(); + + if (!allowUnstableFeatures && + configuredFeaturesToAvoid.containsKey(configuredFeature.hashCode())) + continue; + + + try + { + configuredFeature.place(lodServerWorld, chunkGen, serverWorld.random, chunk.getPos().getWorldPosition()); + } + catch (ConcurrentModificationException | UnsupportedOperationException e) + { + // This will happen. I'm not sure what to do about it + // except pray that it doesn't affect the normal world generation + // in any harmful way. + // Update: this can cause crashes and high CPU usage. + + // Issue #35 + // I tried cloning the config for each feature, but that + // path was blocked since I can't clone lambda methods. + // I tried using a deep cloning library and discovered + // the problem there. + // ( https://github.com/kostaskougios/cloning + // and + // https://github.com/EsotericSoftware/kryo ) + + if (!allowUnstableFeatures) + configuredFeaturesToAvoid.put(configuredFeature.hashCode(), configuredFeature); +// ClientProxy.LOGGER.info(configuredFeaturesToAvoid.mappingCount()); + } + // This will happen when the LodServerWorld + // isn't able to return something that a feature + // generator needs + catch (Exception e) + { + // I'm not sure what happened, print to the log + + e.printStackTrace(); + + if (!allowUnstableFeatures) + configuredFeaturesToAvoid.put(configuredFeature.hashCode(), configuredFeature); +// ClientProxy.LOGGER.info(configuredFeaturesToAvoid.mappingCount()); + } + } + } + } + + // generate a Lod like normal + lodBuilder.generateLodNodeFromChunk(lodDim, new ChunkWrapper(chunk), new LodBuilderConfig(DistanceGenerationMode.FEATURES)); + } + + + /** + * on pre generated chunks 0 - 1 ms + * on un generated chunks 0 - 50 ms + * with the median seeming to hover around 15 - 30 ms + * and outliers in the 100 - 200 ms range + *

+ * Note this should not be multithreaded and does cause server/simulation lag + * (Higher lag for generating than loading) + */ + private void generateWithServer() + { + lodBuilder.generateLodNodeFromChunk(lodDim, new ChunkWrapper(serverWorld.getChunk(pos.x, pos.z, ChunkStatus.FEATURES)), new LodBuilderConfig(DistanceGenerationMode.SERVER)); + } + + + + + + + //================// + // Unused methods // + //================// + + // Sadly I wasn't able to get these to work, + // they are here for documentation purposes + + @SuppressWarnings({ "rawtypes", "unchecked", "unused" }) + private DecoratedFeatureConfig cloneDecoratedFeatureConfig(DecoratedFeatureConfig config) + { + IPlacementConfig placementConfig; + + Class oldConfigClass = config.decorator.config().getClass(); + + if (oldConfigClass == FeatureSpreadConfig.class) + { + FeatureSpreadConfig oldPlacementConfig = (FeatureSpreadConfig) config.decorator.config(); + FeatureSpread oldSpread = oldPlacementConfig.count(); + + placementConfig = new FeatureSpreadConfig(oldSpread); + } + else if (oldConfigClass == DecoratedPlacementConfig.class) + { + DecoratedPlacementConfig oldPlacementConfig = (DecoratedPlacementConfig) config.decorator.config(); + placementConfig = new DecoratedPlacementConfig(oldPlacementConfig.inner(), oldPlacementConfig.outer()); + } + else if (oldConfigClass == NoiseDependant.class) + { + NoiseDependant oldPlacementConfig = (NoiseDependant) config.decorator.config(); + placementConfig = new NoiseDependant(oldPlacementConfig.noiseLevel, oldPlacementConfig.belowNoise, oldPlacementConfig.aboveNoise); + } + else + { +// ClientProxy.LOGGER.debug("unknown decorated placement config: \"" + config.decorator.config().getClass() + "\""); + return config; + } + + + ConfiguredPlacement newPlacement = new ConfiguredPlacement(config.decorator.decorator, placementConfig); + return new DecoratedFeatureConfig(config.feature, newPlacement); + } + + + @SuppressWarnings("unused") + private BlockClusterFeatureConfig cloneBlockClusterFeatureConfig(BlockClusterFeatureConfig config) + { + WeightedBlockStateProvider provider = new WeightedBlockStateProvider(); + provider.weightedList.entries.addAll(((WeightedBlockStateProvider) config.stateProvider).weightedList.entries); + + HashSet whitelist = new HashSet<>(config.whitelist); + + HashSet blacklist = new HashSet<>(config.blacklist); + + + BlockClusterFeatureConfig.Builder builder = new BlockClusterFeatureConfig.Builder(provider, config.blockPlacer); + builder.whitelist(whitelist); + builder.blacklist(blacklist); + builder.xspread(config.xspread); + builder.yspread(config.yspread); + builder.zspread(config.zspread); + if (config.canReplace) + { + builder.canReplace(); + } + if (config.needWater) + { + builder.needWater(); + } + if (config.project) + { + builder.noProjection(); + } + builder.tries(config.tries); + + + return builder.build(); + } + + } + + + /** + * Stops the current genThreads if they are running + * and then recreates the Executor service.

+ *

+ * This is done to clear any outstanding tasks + * that may exist after the player leaves their current world. + * If this isn't done unfinished tasks may be left in the queue + * preventing new LodChunks form being generated. + */ + public static void restartExecutorService() + { + if (genThreads != null && !genThreads.isShutdown()) + { + genThreads.shutdownNow(); + } + genThreads = Executors.newFixedThreadPool(LodConfig.CLIENT.advancedModOptions.threading.numberOfWorldGenerationThreads.get(), new ThreadFactoryBuilder().setNameFormat("Gen-Worker-Thread-%d").build()); + } + + + + + + /* + * performance/generation tests related to + * serverWorld.getChunk(x, z, ChunkStatus. *** ) + + true/false is whether they generated blocks or not + the time is how long it took to generate + + ChunkStatus.EMPTY 0 - 1 ms false (empty, what did you expect? :P) + ChunkStatus.STRUCTURE_REFERENCES 1 - 2 ms false (no height, only generates some chunks) + ChunkStatus.BIOMES 1 - 10 ms false (no height) + ChunkStatus.NOISE 4 - 15 ms true (all blocks are stone) + ChunkStatus.LIQUID_CARVERS 6 - 12 ms true (no snow/trees, just grass) + ChunkStatus.SURFACE 5 - 15 ms true (no snow/trees, just grass) + ChunkStatus.CARVERS 5 - 30 ms true (no snow/trees, just grass) + ChunkStatus.FEATURES 7 - 25 ms true + ChunkStatus.HEIGHTMAPS 20 - 40 ms true + ChunkStatus.LIGHT 20 - 40 ms true + ChunkStatus.FULL 30 - 50 ms true + ChunkStatus.SPAWN 50 - 80 ms true + + + At this point I would suggest using FEATURES, as it generates snow and trees + (and any other object that is needed to make biomes distinct) + + Otherwise, if snow/trees aren't necessary SURFACE is the next fastest (although not by much) + */ +} diff --git a/src/main/java/com/seibel/lod/builders/worldGeneration/LodServerWorld.java b/src/main/java/com/seibel/lod/builders/worldGeneration/LodServerWorld.java new file mode 100644 index 000000000..1229a853c --- /dev/null +++ b/src/main/java/com/seibel/lod/builders/worldGeneration/LodServerWorld.java @@ -0,0 +1,331 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.builders.worldGeneration; + +import java.util.HashMap; +import java.util.List; +import java.util.Random; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import com.seibel.lod.util.LodUtil; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.fluid.Fluid; +import net.minecraft.fluid.FluidState; +import net.minecraft.particles.IParticleData; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.Direction; +import net.minecraft.util.SoundCategory; +import net.minecraft.util.SoundEvent; +import net.minecraft.util.math.AxisAlignedBB; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.SectionPos; +import net.minecraft.util.registry.DynamicRegistries; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.DimensionType; +import net.minecraft.world.EmptyTickList; +import net.minecraft.world.ISeedReader; +import net.minecraft.world.ITickList; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.biome.BiomeManager; +import net.minecraft.world.border.WorldBorder; +import net.minecraft.world.chunk.AbstractChunkProvider; +import net.minecraft.world.chunk.ChunkStatus; +import net.minecraft.world.chunk.IChunk; +import net.minecraft.world.gen.Heightmap; +import net.minecraft.world.gen.Heightmap.Type; +import net.minecraft.world.gen.feature.structure.Structure; +import net.minecraft.world.gen.feature.structure.StructureStart; +import net.minecraft.world.lighting.WorldLightManager; +import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.storage.IWorldInfo; + + +/** + * This is a fake ServerWorld used when generating features. + * This allows us to keep each LodChunk generation independent + * of the actual ServerWorld, allowing us + * to multithread generation. + * @author James Seibel + * @version 7-26-2021 + */ +public class LodServerWorld implements ISeedReader +{ + + public HashMap heightmaps = new HashMap<>(); + + public final IChunk chunk; + + public final ServerWorld serverWorld; + + public LodServerWorld(ServerWorld newServerWorld, IChunk newChunk) + { + chunk = newChunk; + serverWorld = newServerWorld; + } + + + @Override + public int getHeight(Type heightmapType, int x, int z) + { + // make sure the block position is set relative to the chunk + x = x % LodUtil.CHUNK_WIDTH; + x = (x < 0) ? x + 16 : x; + + z = z % LodUtil.CHUNK_WIDTH; + z = (z < 0) ? z + 16 : z; + + return chunk.getOrCreateHeightmapUnprimed(LodUtil.DEFAULT_HEIGHTMAP).getFirstAvailable(x, z); + } + + @Override + public Biome getBiome(BlockPos pos) + { + return chunk.getBiomes().getNoiseBiome(pos.getX() >> 2, pos.getY() >> 2, pos.getZ() >> 2); + } + + @Override + public boolean setBlock(BlockPos pos, BlockState state, int flags, int recursionLeft) + { + return chunk.setBlockState(pos, state, false) == state; + } + + @Override + public BlockState getBlockState(BlockPos pos) + { + return chunk.getBlockState(pos); + } + + @Override + public FluidState getFluidState(BlockPos pos) + { + return chunk.getFluidState(pos); + } + + + @Override + public boolean isStateAtPosition(BlockPos pos, Predicate state) + { + return state.test(chunk.getBlockState(pos)); + } + + @Override + public ITickList getBlockTicks() + { + return EmptyTickList.empty(); + } + + @Override + public IChunk getChunk(int x, int z, ChunkStatus requiredStatus, boolean nonnull) + { + return chunk; + } + + @Override + public Stream> startsForFeature(SectionPos p_241827_1_, Structure p_241827_2_) + { + return serverWorld.startsForFeature(p_241827_1_, p_241827_2_); + } + + @Override + public ITickList getLiquidTicks() + { + return EmptyTickList.empty(); + } + + @Override + public WorldLightManager getLightEngine() + { + return new WorldLightManager(null, false, false); + } + + @Override + public long getSeed() + { + return serverWorld.getSeed(); + } + + @Override + public DynamicRegistries registryAccess() + { + return serverWorld.registryAccess(); + } + + + /** + * All methods below shouldn't be needed + * and thus have been left unimplemented. + */ + + + @Override + public Random getRandom() + { + throw new UnsupportedOperationException("Not Implemented"); + } + + @Override + public void playSound(PlayerEntity player, BlockPos pos, SoundEvent soundIn, SoundCategory category, float volume, + float pitch) + { + throw new UnsupportedOperationException("Not Implemented"); + } + + @Override + public void addParticle(IParticleData particleData, double x, double y, double z, double xSpeed, double ySpeed, + double zSpeed) + { + throw new UnsupportedOperationException("Not Implemented"); + } + + @Override + public BiomeManager getBiomeManager() + { + throw new UnsupportedOperationException("Not Implemented"); + } + + @Override + public int getSeaLevel() + { + throw new UnsupportedOperationException("Not Implemented"); + } + + @Override + public float getShade(Direction p_230487_1_, boolean p_230487_2_) + { + throw new UnsupportedOperationException("Not Implemented"); + } + + @Override + public WorldBorder getWorldBorder() + { + throw new UnsupportedOperationException("Not Implemented"); + } + + @Override + public boolean removeBlock(BlockPos pos, boolean isMoving) + { + throw new UnsupportedOperationException("Not Implemented"); + } + + @Override + public boolean destroyBlock(BlockPos pos, boolean dropBlock, Entity entity, int recursionLeft) + { + throw new UnsupportedOperationException("Not Implemented"); + } + + + @Override + public ServerWorld getLevel() + { + throw new UnsupportedOperationException("Not Implemented"); + } + + + @Override + public AbstractChunkProvider getChunkSource() + { + throw new UnsupportedOperationException("Not Implemented"); + } + + + @Override + public DifficultyInstance getCurrentDifficultyAt(BlockPos arg0) + { + throw new UnsupportedOperationException("Not Implemented"); + } + + + @Override + public IWorldInfo getLevelData() + { + throw new UnsupportedOperationException("Not Implemented"); + } + + + @Override + public void levelEvent(PlayerEntity arg0, int arg1, BlockPos arg2, int arg3) + { + throw new UnsupportedOperationException("Not Implemented"); + + } + + + @Override + public List getEntities(Entity arg0, AxisAlignedBB arg1, Predicate arg2) + { + throw new UnsupportedOperationException("Not Implemented"); + } + + + @Override + public List getEntitiesOfClass(Class arg0, AxisAlignedBB arg1, + Predicate arg2) + { + throw new UnsupportedOperationException("Not Implemented"); + } + + + @Override + public List players() + { + throw new UnsupportedOperationException("Not Implemented"); + } + + + @Override + public int getSkyDarken() + { + throw new UnsupportedOperationException("Not Implemented"); + } + + + @Override + public Biome getUncachedNoiseBiome(int p_225604_1_, int p_225604_2_, int p_225604_3_) + { + throw new UnsupportedOperationException("Not Implemented"); + } + + + @Override + public boolean isClientSide() + { + throw new UnsupportedOperationException("Not Implemented"); + } + + + @Override + public DimensionType dimensionType() + { + throw new UnsupportedOperationException("Not Implemented"); + } + + + @Override + public TileEntity getBlockEntity(BlockPos p_175625_1_) + { + throw new UnsupportedOperationException("Not Implemented"); + } + +} diff --git a/src/main/java/com/seibel/lod/builders/worldGeneration/LodWorldGenerator.java b/src/main/java/com/seibel/lod/builders/worldGeneration/LodWorldGenerator.java new file mode 100644 index 000000000..736aea480 --- /dev/null +++ b/src/main/java/com/seibel/lod/builders/worldGeneration/LodWorldGenerator.java @@ -0,0 +1,204 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.builders.worldGeneration; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import com.seibel.lod.builders.lodBuilding.LodBuilder; +import com.seibel.lod.config.LodConfig; +import com.seibel.lod.enums.DistanceGenerationMode; +import com.seibel.lod.objects.LodDimension; +import com.seibel.lod.objects.PosToGenerateContainer; +import com.seibel.lod.render.LodRenderer; +import com.seibel.lod.util.DetailDistanceUtil; +import com.seibel.lod.util.LevelPosUtil; +import com.seibel.lod.util.LodThreadFactory; +import com.seibel.lod.util.LodUtil; +import com.seibel.lod.wrappers.MinecraftWrapper; + +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.server.ServerWorld; +import net.minecraftforge.common.WorldWorkerManager; + +/** + * A singleton that handles all long distance LOD world generation. + * @author James Seibel + * @version 9-25-2021 + */ +public class LodWorldGenerator +{ + public final MinecraftWrapper mc = MinecraftWrapper.INSTANCE; + + /** This holds the thread used to generate new LODs off the main thread. */ + private final ExecutorService mainGenThread = Executors.newSingleThreadExecutor(new LodThreadFactory(this.getClass().getSimpleName() + " world generator")); + + /** we only want to queue up one generator thread at a time */ + private boolean generatorThreadRunning = false; + + /** + * How many chunks to generate outside the player's view distance at one + * time. (or more specifically how many requests to make at one time). I + * multiply by 8 to make sure there is always a buffer of chunk requests, to + * make sure the CPU is always busy, and we can generate LODs as quickly as + * possible. + */ + public int maxChunkGenRequests; + + /** + * This keeps track of how many chunk generation requests are on going. This is + * to limit how many chunks are queued at once. To prevent chunks from being + * generated for a long time in an area the player is no longer in. + */ + public final AtomicInteger numberOfChunksWaitingToGenerate = new AtomicInteger(0); + + public final Set positionsWaitingToBeGenerated = new HashSet<>(); + + /** + * Singleton copy of this object + */ + public static final LodWorldGenerator INSTANCE = new LodWorldGenerator(); + + + private LodWorldGenerator() + { + + } + + /** + * Queues up LodNodeGenWorkers for the given lodDimension. + * @param renderer needed so the LodNodeGenWorkers can flag that the + * buffers need to be rebuilt. + */ + public void queueGenerationRequests(LodDimension lodDim, LodRenderer renderer, LodBuilder lodBuilder) + { + if (LodConfig.CLIENT.worldGenerator.distanceGenerationMode.get() != DistanceGenerationMode.NONE + && !generatorThreadRunning + && mc.hasSinglePlayerServer()) + { + // the thread is now running, don't queue up another thread + generatorThreadRunning = true; + + // just in case the config changed + maxChunkGenRequests = LodConfig.CLIENT.advancedModOptions.threading.numberOfWorldGenerationThreads.get() * 8; + + Thread generatorThread = new Thread(() -> + { + try + { + // round the player's block position down to the nearest chunk BlockPos + int playerPosX = mc.getPlayer().blockPosition().getX(); + int playerPosZ = mc.getPlayer().blockPosition().getZ(); + + + //=======================================// + // fill in positionsWaitingToBeGenerated // + //=======================================// + + ServerWorld serverWorld = LodUtil.getServerWorldFromDimension(lodDim.dimension); + + PosToGenerateContainer posToGenerate = lodDim.getPosToGenerate( + maxChunkGenRequests, + playerPosX, + playerPosZ); + + + byte detailLevel; + int posX; + int posZ; + int nearIndex = 0; + int farIndex = 0; + + for (int i = 0; i < posToGenerate.getNumberOfPos(); i++) + { + // I wish there was a way to compress this code, but I'm not aware of + // an easy way to do so. + + // add the near positions + if (posToGenerate.getNthDetail(nearIndex, true) != 0 && nearIndex < posToGenerate.getNumberOfNearPos()) + { + detailLevel = (byte) (posToGenerate.getNthDetail(nearIndex, true) - 1); + posX = posToGenerate.getNthPosX(nearIndex, true); + posZ = posToGenerate.getNthPosZ(nearIndex, true); + nearIndex++; + + ChunkPos chunkPos = new ChunkPos(LevelPosUtil.getChunkPos(detailLevel, posX), LevelPosUtil.getChunkPos(detailLevel, posZ)); + + // prevent generating the same chunk multiple times + if (positionsWaitingToBeGenerated.contains(chunkPos)) + continue; + + // don't add more to the generation queue then allowed + if (numberOfChunksWaitingToGenerate.get() >= maxChunkGenRequests) + break; + + positionsWaitingToBeGenerated.add(chunkPos); + numberOfChunksWaitingToGenerate.addAndGet(1); + LodGenWorker genWorker = new LodGenWorker(chunkPos, DetailDistanceUtil.getDistanceGenerationMode(detailLevel), lodBuilder, lodDim, serverWorld); + WorldWorkerManager.addWorker(genWorker); + } + + + // add the far positions + if (posToGenerate.getNthDetail(farIndex, false) != 0 && farIndex < posToGenerate.getNumberOfFarPos()) + { + detailLevel = (byte) (posToGenerate.getNthDetail(farIndex, false) - 1); + posX = posToGenerate.getNthPosX(farIndex, false); + posZ = posToGenerate.getNthPosZ(farIndex, false); + farIndex++; + + ChunkPos chunkPos = new ChunkPos(LevelPosUtil.getChunkPos(detailLevel, posX), LevelPosUtil.getChunkPos(detailLevel, posZ)); + + // don't add more to the generation queue then allowed + if (numberOfChunksWaitingToGenerate.get() >= maxChunkGenRequests) + continue; + //break; + + // prevent generating the same chunk multiple times + if (positionsWaitingToBeGenerated.contains(chunkPos)) + continue; + + positionsWaitingToBeGenerated.add(chunkPos); + numberOfChunksWaitingToGenerate.addAndGet(1); + LodGenWorker genWorker = new LodGenWorker(chunkPos, DetailDistanceUtil.getDistanceGenerationMode(detailLevel), lodBuilder, lodDim, serverWorld); + WorldWorkerManager.addWorker(genWorker); + } + } + + } + catch (Exception e) + { + // this shouldn't ever happen, but just in case + e.printStackTrace(); + } + finally + { + generatorThreadRunning = false; + } + }); + + mainGenThread.execute(generatorThread); + } // if distanceGenerationMode != DistanceGenerationMode.NONE && !generatorThreadRunning + } // queueGenerationRequests + +} diff --git a/src/main/java/com/seibel/lod/config/LodConfig.java b/src/main/java/com/seibel/lod/config/LodConfig.java new file mode 100644 index 000000000..004b1fab2 --- /dev/null +++ b/src/main/java/com/seibel/lod/config/LodConfig.java @@ -0,0 +1,591 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.config; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.logging.log4j.LogManager; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import com.electronwill.nightconfig.core.io.WritingMode; +import com.seibel.lod.ModInfo; +import com.seibel.lod.enums.BlockToAvoid; +import com.seibel.lod.enums.BufferRebuildTimes; +import com.seibel.lod.enums.DebugMode; +import com.seibel.lod.enums.DistanceGenerationMode; +import com.seibel.lod.enums.FogDistance; +import com.seibel.lod.enums.FogDrawOverride; +import com.seibel.lod.enums.GenerationPriority; +import com.seibel.lod.enums.GpuUploadMethod; +import com.seibel.lod.enums.HorizontalQuality; +import com.seibel.lod.enums.HorizontalResolution; +import com.seibel.lod.enums.HorizontalScale; +import com.seibel.lod.enums.LodTemplate; +import com.seibel.lod.enums.VanillaOverdraw; +import com.seibel.lod.enums.VerticalQuality; +import com.seibel.lod.util.LodUtil; + +import net.minecraftforge.common.ForgeConfigSpec; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.config.ModConfig; + +/** + * This handles any configuration the user has access to. + * @author Leonardo Amato + * @author James Seibel + * @version 10-25-2021 + */ +@Mod.EventBusSubscriber +public class LodConfig +{ + // CONFIG STRUCTURE + // -> Client + // | + // |-> Graphics + // | |-> QualityOption + // | |-> FogQualityOption + // | |-> AdvancedGraphicsOption + // | + // |-> World Generation + // | + // |-> Advanced Mod Option + // |-> Threads + // |-> Buffers + // |-> Debugging + + + + public static class Client + { + public final Graphics graphics; + public final WorldGenerator worldGenerator; + public final AdvancedModOptions advancedModOptions; + + + //================// + // Client Configs // + //================// + public Client(ForgeConfigSpec.Builder builder) + { + builder.push(this.getClass().getSimpleName()); + { + graphics = new Graphics(builder); + worldGenerator = new WorldGenerator(builder); + advancedModOptions = new AdvancedModOptions(builder); + } + builder.pop(); + } + + + //==================// + // Graphics Configs // + //==================// + public static class Graphics + { + + public final QualityOption qualityOption; + public final FogQualityOption fogQualityOption; + public final AdvancedGraphicsOption advancedGraphicsOption; + + Graphics(ForgeConfigSpec.Builder builder) + { + builder.comment("These settings control how the mod will look in game").push("Graphics"); + { + qualityOption = new QualityOption(builder); + advancedGraphicsOption = new AdvancedGraphicsOption(builder); + fogQualityOption = new FogQualityOption(builder); + } + builder.pop(); + } + + + public static class QualityOption + { + public final ForgeConfigSpec.EnumValue drawResolution; + + public final ForgeConfigSpec.IntValue lodChunkRenderDistance; + + public final ForgeConfigSpec.EnumValue verticalQuality; + + public final ForgeConfigSpec.EnumValue horizontalScale; + + public final ForgeConfigSpec.EnumValue horizontalQuality; + + + QualityOption(ForgeConfigSpec.Builder builder) + { + builder.comment("These settings control how detailed the fake chunks will be.").push(this.getClass().getSimpleName()); + + verticalQuality = builder + .comment("\n\n" + + " This indicates how detailed fake chunks will represent \n" + + " overhangs, caves, floating islands, ect. \n" + + " Higher options will use more memory and increase GPU usage. \n" + + " " + VerticalQuality.LOW + ": uses at max 2 columns per position. \n" + + " " + VerticalQuality.MEDIUM + ": uses at max 4 columns per position. \n" + + " " + VerticalQuality.HIGH + ": uses at max 8 columns per position. \n") + .defineEnum("Vertical Quality", VerticalQuality.MEDIUM); + + horizontalScale = builder + .comment("\n\n" + + " This indicates how quickly fake chunks drop off in quality. \n" + + " " + HorizontalScale.LOW + ": quality drops every " + HorizontalScale.LOW.distanceUnit / 16 + " chunks. \n" + + " " + HorizontalScale.MEDIUM + ": quality drops every " + HorizontalScale.MEDIUM.distanceUnit / 16 + " chunks. \n" + + " " + HorizontalScale.HIGH + ": quality drops every " + HorizontalScale.HIGH.distanceUnit / 16 + " chunks. \n") + .defineEnum("Horizontal Scale", HorizontalScale.MEDIUM); + + horizontalQuality = builder + .comment("\n\n" + + " This indicates the exponential base of the quadratic drop-off \n" + + " " + HorizontalQuality.LOWEST + ": base " + HorizontalQuality.LOWEST.quadraticBase + ". \n" + + " " + HorizontalQuality.LOW + ": base " + HorizontalQuality.LOW.quadraticBase + ". \n" + + " " + HorizontalQuality.MEDIUM + ": base " + HorizontalQuality.MEDIUM.quadraticBase + ". \n" + + " " + HorizontalQuality.HIGH + ": base " + HorizontalQuality.HIGH.quadraticBase + ". \n") + .defineEnum("Horizontal Quality", HorizontalQuality.MEDIUM); + + drawResolution = builder + .comment("\n\n" + + " What is the maximum detail fake chunks should be drawn at? \n" + + " " + HorizontalResolution.CHUNK + ": render 1 LOD for each Chunk. \n" + + " " + HorizontalResolution.HALF_CHUNK + ": render 4 LODs for each Chunk. \n" + + " " + HorizontalResolution.FOUR_BLOCKS + ": render 16 LODs for each Chunk. \n" + + " " + HorizontalResolution.TWO_BLOCKS + ": render 64 LODs for each Chunk. \n" + + " " + HorizontalResolution.BLOCK + ": render 256 LODs for each Chunk. \n") + .defineEnum("Block size", HorizontalResolution.BLOCK); + + lodChunkRenderDistance = builder + .comment("\n\n" + + " The mod's render distance, measured in chunks. \n") + .defineInRange("Lod Render Distance", 64, 32, 1024); + + builder.pop(); + } + } + + + public static class FogQualityOption + { + public final ForgeConfigSpec.EnumValue fogDistance; + + public final ForgeConfigSpec.EnumValue fogDrawOverride; + + public final ForgeConfigSpec.BooleanValue disableVanillaFog; + + FogQualityOption(ForgeConfigSpec.Builder builder) + { + + builder.comment("These settings control the fog quality.").push(this.getClass().getSimpleName()); + + fogDistance = builder + .comment("\n\n" + + " At what distance should Fog be drawn on the fake chunks? \n" + + " If the fog cuts off abruptly or you are using Optifine's \"fast\" fog option \n" + + " set this to " + FogDistance.NEAR + " or " + FogDistance.FAR + ". \n") + .defineEnum("Fog Distance", FogDistance.FAR); + + fogDrawOverride = builder + .comment("\n\n" + + " When should fog be drawn? \n" + + " " + FogDrawOverride.OPTIFINE_SETTING + ": Use whatever Fog setting Optifine is using. If Optifine isn't installed this defaults to " + FogDrawOverride.FANCY + ". \n" + + " " + FogDrawOverride.NO_FOG + ": Never draw fog on the LODs \n" + + " " + FogDrawOverride.FAST + ": Always draw fast fog on the LODs \n" + + " " + FogDrawOverride.FANCY + ": Always draw fancy fog on the LODs (if your graphics card supports it) \n") + .defineEnum("Fog Draw Override", FogDrawOverride.FANCY); + + disableVanillaFog = builder + .comment("\n\n" + + " If true disable Minecraft's fog. \n\n" + + "" + + " Experimental! May cause issues with Sodium. \n\n" + + "" + + " Unlike Optifine or Sodium's fog disabling option this won't change \n" + + " performance (we don't actually disable the fog, we just tell it to render a infinite distance away). \n" + + " May or may not play nice with other mods that edit fog. \n") + .define("Experimental Disable Vanilla Fog", false); + + builder.pop(); + } + } + + + public static class AdvancedGraphicsOption + { + public final ForgeConfigSpec.EnumValue lodTemplate; + + public final ForgeConfigSpec.BooleanValue disableDirectionalCulling; + + public final ForgeConfigSpec.BooleanValue alwaysDrawAtMaxQuality; + + public final ForgeConfigSpec.EnumValue vanillaOverdraw; + + public final ForgeConfigSpec.EnumValue gpuUploadMethod; + + public final ForgeConfigSpec.BooleanValue useExtendedNearClipPlane; + + AdvancedGraphicsOption(ForgeConfigSpec.Builder builder) + { + + builder.comment("Advanced graphics option for the mod").push(this.getClass().getSimpleName()); + + lodTemplate = builder + .comment("\n\n" + + " How should the LODs be drawn? \n" + + " NOTE: Currently only " + LodTemplate.CUBIC + " is implemented! \n" + + " \n" + + " " + LodTemplate.CUBIC + ": LOD Chunks are drawn as rectangular prisms (boxes). \n" + + " " + LodTemplate.TRIANGULAR + ": LOD Chunks smoothly transition between other. \n" + + " " + LodTemplate.DYNAMIC + ": LOD Chunks smoothly transition between each other, \n" + + " " + " unless a neighboring chunk is at a significantly different height. \n") + .defineEnum("LOD Template", LodTemplate.CUBIC); + + disableDirectionalCulling = builder + .comment("\n\n" + + " If false fake chunks behind the player's camera \n" + + " aren't drawn, increasing performance. \n\n" + + "" + + " If true all LODs are drawn, even those behind \n" + + " the player's camera, decreasing performance. \n\n" + + "" + + " Disable this if you see LODs disappearing. \n" + + " (Which may happen if you are using a camera mod) \n") + .define("Disable Directional Culling", false); + + alwaysDrawAtMaxQuality = builder + .comment("\n\n" + + " Disable quality falloff, \n" + + " all fake chunks will be drawn at the highest \n" + + " available detail level. \n\n" + + " " + + " WARNING: \n" + + " This could cause a Out Of Memory crash on render \n" + + " distances higher than 128 \n") + .define("Always Use Max Quality", false); + + vanillaOverdraw = builder + .comment("\n\n" + + " How often should LODs be drawn on top of regular chunks? \n" + + " HALF and ALWAYS will prevent holes in the world, but may look odd for transparent blocks or in caves. \n\n" + + " " + VanillaOverdraw.NEVER + ": LODs won't render on top of vanilla chunks. \n" + + " " + VanillaOverdraw.BORDER + ": LODs will render only on the border of vanilla chunks preventing only some holes in the world. \n" + + " " + VanillaOverdraw.DYNAMIC + ": LODs will render on top of distant vanilla chunks to hide delayed loading. \n" + + " " + " More effective on higher render distances. \n" + + " " + " For vanilla render distances less than or equal to " + LodUtil.MINIMUM_RENDER_DISTANCE_FOR_PARTIAL_OVERDRAW + " \n" + + " " + " " + VanillaOverdraw.NEVER + " or " + VanillaOverdraw.ALWAYS + " may be used depending on the dimension. \n" + + " " + VanillaOverdraw.ALWAYS + ": LODs will render on all vanilla chunks preventing holes in the world. \n") + .defineEnum("Vanilla Overdraw", VanillaOverdraw.DYNAMIC); + + gpuUploadMethod = builder + .comment("\n\n" + + " What method should be used to upload geometry to the GPU? \n\\n" + + "" + + " " + GpuUploadMethod.BUFFER_STORAGE + ": Default if OpenGL 4.5 is supported. Fast rendering, no stuttering. \n" + + " " + GpuUploadMethod.SUB_DATA + ": Default if OpenGL 4.5 is NOT supported. Fast rendering but may stutter when uploading. \n" + + " " + GpuUploadMethod.BUFFER_MAPPING + ": Slow rendering but won't stutter when uploading. Possibly better than " + GpuUploadMethod.SUB_DATA + " if using a integrated GPU. \n") + .defineEnum("GPU Upload Method", GpuUploadMethod.BUFFER_STORAGE); + + // This is a temporary fix (like vanilla overdraw) + // hopefully we can remove both once we get individual chunk rendering figured out + useExtendedNearClipPlane = builder + .comment("\n\n" + + " Will prevent some overdraw issues, but may cause nearby fake chunks to render incorrectly \n" + + " especially when in/near an ocean. \n") + .define("Use Extended Near Clip Plane", false); + + + builder.pop(); + } + } + } + + + + + //========================// + // WorldGenerator Configs // + //========================// + public static class WorldGenerator + { + public final ForgeConfigSpec.EnumValue generationPriority; + public final ForgeConfigSpec.EnumValue distanceGenerationMode; + public final ForgeConfigSpec.BooleanValue allowUnstableFeatureGeneration; + public final ForgeConfigSpec.EnumValue blockToAvoid; + //public final ForgeConfigSpec.BooleanValue useExperimentalPreGenLoading; + + WorldGenerator(ForgeConfigSpec.Builder builder) + { + builder.comment("These settings control how fake chunks outside your normal view range are generated.").push("Generation"); + + generationPriority = builder + .comment("\n\n" + + " " + GenerationPriority.FAR_FIRST + " \n" + + " LODs are generated from low to high detail \n" + + " with a small priority for far away regions. \n" + + " This fills in the world fastest. \n\n" + + "" + + " " + GenerationPriority.NEAR_FIRST + " \n" + + " LODs are generated around the player \n" + + " in a spiral, similar to vanilla minecraft. \n") + .defineEnum("Generation Priority", GenerationPriority.FAR_FIRST); + + distanceGenerationMode = builder + .comment("\n\n" + + " Note: The times listed here are the amount of time it took \n" + + " one of the developer's PC to generate 1 chunk, \n" + + " and are included so you can compare the \n" + + " different generation options. Your mileage may vary. \n" + + "\n" + + + " " + DistanceGenerationMode.NONE + " \n" + + " Don't run the distance generator. \n" + + + "\n" + + " " + DistanceGenerationMode.BIOME_ONLY + " \n" + + " Only generate the biomes and use the biome's \n" + + " grass color, water color, or snow color. \n" + + " Doesn't generate height, everything is shown at sea level. \n" + + " Multithreaded - Fastest (2-5 ms) \n" + + + "\n" + + " " + DistanceGenerationMode.BIOME_ONLY_SIMULATE_HEIGHT + " \n" + + " Same as BIOME_ONLY, except instead \n" + + " of always using sea level as the LOD height \n" + + " different biome types (mountain, ocean, forest, etc.) \n" + + " use predetermined heights to simulate having height data. \n" + + " Multithreaded - Fastest (2-5 ms) \n" + + + "\n" + + " " + DistanceGenerationMode.SURFACE + " \n" + + " Generate the world surface, \n" + + " this does NOT include trees, \n" + + " or structures. \n" + + " Multithreaded - Faster (10-20 ms) \n" + + + "\n" + + " " + DistanceGenerationMode.FEATURES + " \n" + + " Generate everything except structures. \n" + + " WARNING: This may cause world generation bugs or instability! \n" + + " Multithreaded - Fast (15-20 ms) \n" + + + "\n" + + " " + DistanceGenerationMode.SERVER + " \n" + + " Ask the server to generate/load each chunk. \n" + + " This will show player made structures, which can \n" + + " be useful if you are adding the mod to a pre-existing world. \n" + + " This is the most compatible, but causes server/simulation lag. \n" + + " SingleThreaded - Slow (15-50 ms, with spikes up to 200 ms) \n") + .defineEnum("Distance Generation Mode", DistanceGenerationMode.SURFACE); + + allowUnstableFeatureGeneration = builder + .comment("\n\n" + + " When using the " + DistanceGenerationMode.FEATURES + " generation mode \n" + + " some features may not be thread safe, which could \n" + + " cause instability and crashes. \n" + + " By default (false) those features are skipped, \n" + + " improving stability, but decreasing how many features are \n" + + " actually generated. \n" + + " (for example: some tree generation is unstable, \n" + + " so some trees may not be generated.) \n" + + " By setting this to true, all features will be generated, \n" + + " but your game will be more unstable and crashes may occur. \n" + + " \n" + + " I would love to remove this option and always generate everything, \n" + + " but I'm not sure how to do that. \n" + + " If you are a Java wizard, check out the git issue here: \n" + + " https://gitlab.com/jeseibel/minecraft-lod-mod/-/issues/35 \n") + .define("Allow Unstable Feature Generation", false); + + blockToAvoid = builder + .comment("\n\n" + + " " + BlockToAvoid.NONE + ": Use all blocks when generating fake chunks \n\n" + + "" + + " " + BlockToAvoid.NON_FULL + ": Only use full blocks when generating fake chunks (ignores slabs, lanterns, torches, grass, etc.) \n\n" + + "" + + " " + BlockToAvoid.NO_COLLISION + ": Only use solid blocks when generating fake chunks (ignores grass, torches, etc.) \n" + + "" + + " " + BlockToAvoid.BOTH + ": Only use full solid blocks when generating fake chunks \n" + + "\n") + .defineEnum("Block to avoid", BlockToAvoid.BOTH); + + /*useExperimentalPreGenLoading = builder + .comment("\n\n" + + " if a chunk has been pre-generated, then the mod would use the real chunk for the \n" + + "fake chunk creation. May require a deletion of the lod file to see the result. \n") + .define("Use pre-generated chunks", false);*/ + builder.pop(); + } + } + + + + + //============================// + // AdvancedModOptions Configs // + //============================// + public static class AdvancedModOptions + { + + public final Threading threading; + public final Debugging debugging; + public final Buffers buffers; + + public AdvancedModOptions(ForgeConfigSpec.Builder builder) + { + builder.comment("Advanced mod settings").push(this.getClass().getSimpleName()); + { + threading = new Threading(builder); + debugging = new Debugging(builder); + buffers = new Buffers(builder); + } + builder.pop(); + } + + public static class Threading + { + public final ForgeConfigSpec.IntValue numberOfWorldGenerationThreads; + public final ForgeConfigSpec.IntValue numberOfBufferBuilderThreads; + + Threading(ForgeConfigSpec.Builder builder) + { + builder.comment("These settings control how many CPU threads the mod uses for different tasks.").push(this.getClass().getSimpleName()); + + numberOfWorldGenerationThreads = builder + .comment("\n\n" + + " This is how many threads are used when generating LODs outside \n" + + " the normal render distance. \n" + + " If you experience stuttering when generating distant LODs, decrease \n" + + " this number. If you want to increase LOD generation speed, \n" + + " increase this number. \n\n" + + "" + + " The maximum value is the number of logical processors on your CPU. \n" + + " Requires a restart to take effect. \n") + .defineInRange("numberOfWorldGenerationThreads", Math.max(1, Runtime.getRuntime().availableProcessors() / 2), 1, Runtime.getRuntime().availableProcessors()); + + numberOfBufferBuilderThreads = builder + .comment("\n\n" + + " This is how many threads are used when building vertex buffers \n" + + " (The things sent to your GPU to draw the fake chunks). \n" + + " If you experience high CPU usage when NOT generating distant \n" + + " fake chunks, lower this number. \n" + + " \n" + + " The maximum value is the number of logical processors on your CPU. \n" + + " Requires a restart to take effect. \n") + .defineInRange("numberOfBufferBuilderThreads", Math.max(1, Runtime.getRuntime().availableProcessors() / 2), 1, Runtime.getRuntime().availableProcessors()); + + builder.pop(); + } + } + + + + + //===============// + // Debug Options // + //===============// + public static class Debugging + { + public final ForgeConfigSpec.BooleanValue drawLods; + public final ForgeConfigSpec.EnumValue debugMode; + public final ForgeConfigSpec.BooleanValue enableDebugKeybindings; + + Debugging(ForgeConfigSpec.Builder builder) + { + builder.comment("These settings can be used to look for bugs, or see how certain aspects of the mod work.").push(this.getClass().getSimpleName()); + + drawLods = builder + .comment("\n\n" + + " If true, the mod is enabled and fake chunks will be drawn. \n" + + " If false, the mod will still generate fake chunks, \n" + + " but they won't be rendered. \n") + .define("Enable Rendering", true); + + debugMode = builder + .comment("\n\n" + + " " + DebugMode.OFF + ": Fake chunks will be drawn with their normal colors. \n" + + " " + DebugMode.SHOW_DETAIL + ": Fake chunks color will be based on their detail level. \n" + + " " + DebugMode.SHOW_DETAIL_WIREFRAME + ": Fake chunks color will be based on their detail level, drawn as a wireframe. \n") + .defineEnum("Debug Mode", DebugMode.OFF); + + enableDebugKeybindings = builder + .comment("\n\n" + + " If true the F4 key can be used to cycle through the different debug modes. \n" + + " and the F6 key can be used to enable and disable LOD rendering.") + .define("Enable Debug Keybinding", false); + + builder.pop(); + } + } + + + public static class Buffers + { + public final ForgeConfigSpec.EnumValue rebuildTimes; + + Buffers(ForgeConfigSpec.Builder builder) + { + builder.comment("These settings affect how often geometry is are built.").push(this.getClass().getSimpleName()); + + rebuildTimes = builder + .comment("\n\n" + + " How frequently should geometry be rebuilt and sent to the GPU? \n" + + " Higher settings may cause stuttering, but will prevent holes in the world \n") + .defineEnum("rebuildFrequency", BufferRebuildTimes.NORMAL); + + builder.pop(); + } + } + } + } + + + /** {@link Path} to the configuration file of this mod */ + private static final Path CONFIG_PATH = Paths.get("config", ModInfo.NAME + ".toml"); + + public static final ForgeConfigSpec CLIENT_SPEC; + public static final Client CLIENT; + + static + { + final Pair specPair = new ForgeConfigSpec.Builder().configure(Client::new); + CLIENT_SPEC = specPair.getRight(); + CLIENT = specPair.getLeft(); + CommentedFileConfig clientConfig = CommentedFileConfig.builder(CONFIG_PATH) + .writingMode(WritingMode.REPLACE) + .build(); + clientConfig.load(); + clientConfig.save(); + CLIENT_SPEC.setConfig(clientConfig); + } + + @SubscribeEvent + public static void onLoad(final ModConfig.Loading configEvent) + { + LogManager.getLogger().debug(ModInfo.NAME, "Loaded forge config file {}", configEvent.getConfig().getFileName()); + } + + @SubscribeEvent + public static void onFileChange(final ModConfig.Reloading configEvent) + { + LogManager.getLogger().debug(ModInfo.NAME, "Forge config just got changed on the file system!"); + } + +} diff --git a/src/main/java/com/seibel/lod/enums/BlockToAvoid.java b/src/main/java/com/seibel/lod/enums/BlockToAvoid.java new file mode 100644 index 000000000..a09b5d68f --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/BlockToAvoid.java @@ -0,0 +1,47 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +/** + * heightmap
+ * multi_lod
+ * + * @author Leonardo Amato + * @version 19-10-2021 + */ +public enum BlockToAvoid +{ + NONE(false, false), + + NON_FULL(true, false), + + NO_COLLISION(false, true), + + BOTH(true, true); + + public final boolean nonFull; + public final boolean noCollision; + + BlockToAvoid(boolean nonFull, boolean noCollision) + { + this.nonFull = nonFull; + this.noCollision = noCollision; + } +} \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/enums/BufferRebuildTimes.java b/src/main/java/com/seibel/lod/enums/BufferRebuildTimes.java new file mode 100644 index 000000000..0ae515b1f --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/BufferRebuildTimes.java @@ -0,0 +1,49 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +/** + * Near_First
+ * Far_First
+ *
+ * Determines how fast the buffers need to be regenerated + * + * @author Leonardo Amato + * @version 9-25-2021 + */ +public enum BufferRebuildTimes +{ + FREQUENT(1000, 500, 2500), + + NORMAL(2000, 1000, 5000), + + RARE(5000, 2000, 10000); + + public final int playerMoveTimeout; + public final int renderedChunkTimeout; + public final int chunkChangeTimeout; + + BufferRebuildTimes(int playerMoveTimeout, int renderedChunkTimeout, int chunkChangeTimeout) + { + this.playerMoveTimeout = playerMoveTimeout; + this.renderedChunkTimeout = renderedChunkTimeout; + this.chunkChangeTimeout = chunkChangeTimeout; + } +} diff --git a/src/main/java/com/seibel/lod/enums/DebugMode.java b/src/main/java/com/seibel/lod/enums/DebugMode.java new file mode 100644 index 000000000..1b68f2a6f --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/DebugMode.java @@ -0,0 +1,54 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +/** + * off, detail, detail wireframe + * + * @author James Seibel + * @version 8-28-2021 + */ +public enum DebugMode +{ + /** LODs are rendered normally */ + OFF, + + /** LOD colors are based on their detail */ + SHOW_DETAIL, + + /** LOD colors are based on their detail, and draws in wireframe. */ + SHOW_DETAIL_WIREFRAME; + + /** used when cycling through the different modes */ + private DebugMode next; + + static + { + OFF.next = SHOW_DETAIL; + SHOW_DETAIL.next = SHOW_DETAIL_WIREFRAME; + SHOW_DETAIL_WIREFRAME.next = OFF; + } + + /** returns the next debug mode */ + public DebugMode getNext() + { + return this.next; + } +} diff --git a/src/main/java/com/seibel/lod/enums/DistanceGenerationMode.java b/src/main/java/com/seibel/lod/enums/DistanceGenerationMode.java new file mode 100644 index 000000000..3060ec4a4 --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/DistanceGenerationMode.java @@ -0,0 +1,95 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +/** + * NONE
+ * BIOME_ONLY
+ * BIOME_ONLY_SIMULATE_HEIGHT
+ * SURFACE
+ * FEATURES
+ * SERVER

+ *

+ * In order of fastest to slowest. + * + * @author James Seibel + * @author Leonardo Amato + * @version 8-7-2021 + */ +public enum DistanceGenerationMode +{ + /** + * Don't generate anything + */ + NONE((byte) 0), + + /** + * Only generate the biomes and use biome + * grass/foliage color, water color, or ice color + * to generate the color. + * Doesn't generate height, everything is shown at sea level. + * Multithreaded - Fastest (2-5 ms) + */ + BIOME_ONLY((byte) 1), + + /** + * Same as BIOME_ONLY, except instead + * of always using sea level as the LOD height + * different biome types (mountain, ocean, forest, etc.) + * use predetermined heights to simulate having height data. + */ + BIOME_ONLY_SIMULATE_HEIGHT((byte) 2), + + /** + * Generate the world surface, + * this does NOT include caves, trees, + * or structures. + * Multithreaded - Faster (10-20 ms) + */ + SURFACE((byte) 3), + + /** + * Generate everything except structures. + * NOTE: This may cause world generation bugs or instability, + * since some features cause concurrentModification exceptions. + * Multithreaded - Fast (15-20 ms) + */ + FEATURES((byte) 4), + + /** + * Ask the server to generate/load each chunk. + * This is the most compatible, but causes server/simulation lag. + * This will also show player made structures if you + * are adding the mod to a pre-existing world. + * Singlethreaded - Slow (15-50 ms, with spikes up to 200 ms) + */ + SERVER((byte) 5); + + + /** + * The higher the number the more complete the generation is. + */ + public final byte complexity; + + DistanceGenerationMode(byte complexity) + { + this.complexity = complexity; + } +} diff --git a/src/main/java/com/seibel/lod/enums/FogDistance.java b/src/main/java/com/seibel/lod/enums/FogDistance.java new file mode 100644 index 000000000..b589a57c5 --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/FogDistance.java @@ -0,0 +1,38 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +/** + * NEAR, FAR, or NEAR_AND_FAR. + * + * @author James Seibel + * @version 02-14-2021 + */ +public enum FogDistance +{ + /** good for fast or fancy fog qualities. */ + NEAR, + + /** good for fast or fancy fog qualities. */ + FAR, + + /** only looks good if the fog quality is set to Fancy. */ + NEAR_AND_FAR +} \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/enums/FogDrawOverride.java b/src/main/java/com/seibel/lod/enums/FogDrawOverride.java new file mode 100644 index 000000000..57ec7a3af --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/FogDrawOverride.java @@ -0,0 +1,47 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +/** + * USE_OPTIFINE_FOG_SETTING,
+ * NEVER_DRAW_FOG,
+ * ALWAYS_DRAW_FOG_FAST,
+ * ALWAYS_DRAW_FOG_FANCY
+ * + * @author James Seibel + * @version 7-3-2021 + */ +public enum FogDrawOverride +{ + /** + * Use whatever Fog setting optifine is using. + * If optifine isn't installed this defaults to ALWAYS_DRAW_FOG. + */ + OPTIFINE_SETTING, + + /** Never draw fog on the LODs */ + NO_FOG, + + /** Always draw fast fog on the LODs */ + FAST, + + /** Always draw fancy fog on the LODs */ + FANCY +} \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/enums/FogQuality.java b/src/main/java/com/seibel/lod/enums/FogQuality.java new file mode 100644 index 000000000..d192a33f4 --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/FogQuality.java @@ -0,0 +1,33 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +/** + * fast, fancy, or off + * + * @author James Seibel + * @version 02-14-2021 + */ +public enum FogQuality +{ + FAST, + FANCY, + OFF +} diff --git a/src/main/java/com/seibel/lod/enums/GenerationPriority.java b/src/main/java/com/seibel/lod/enums/GenerationPriority.java new file mode 100644 index 000000000..63c6b9d7f --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/GenerationPriority.java @@ -0,0 +1,37 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +/** + * Near_First
+ * Far_First
+ *
+ * Determines which LODs should have priority when generating + * outside the normal view distance. + * + * @author Leonardo Amato + * @version 9-25-2021 + */ +public enum GenerationPriority +{ + NEAR_FIRST, + + FAR_FIRST +} diff --git a/src/main/java/com/seibel/lod/enums/GlProxyContext.java b/src/main/java/com/seibel/lod/enums/GlProxyContext.java new file mode 100644 index 000000000..512b46c7b --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/GlProxyContext.java @@ -0,0 +1,38 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +/** + * Minecraft, Lod_Builder, None + * + * @author James Seibel + * @version 10-1-2021 + */ +public enum GlProxyContext +{ + /** Minecraft's render thread */ + MINECRAFT, + + /** The context we send buffers to the GPU on */ + LOD_BUILDER, + + /** used to un-bind threads */ + NONE, +} \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/enums/GpuUploadMethod.java b/src/main/java/com/seibel/lod/enums/GpuUploadMethod.java new file mode 100644 index 000000000..b88be18be --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/GpuUploadMethod.java @@ -0,0 +1,38 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +/** + * Buffer_Storage, Sub_Data, Buffer_Mapping + * + * @author James Seibel + * @version 10-23-2021 + */ +public enum GpuUploadMethod +{ + /** Default if OpenGL 4.5 is supported. Fast rendering, no stuttering. */ + BUFFER_STORAGE, + + /** Default if OpenGL 4.5 is NOT supported. Fast rendering but may stutter when uploading. */ + SUB_DATA, + + /** May end up storing buffers in System memory. Slower rendering but won't stutter when uploading. */ + BUFFER_MAPPING, +} \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/enums/HorizontalQuality.java b/src/main/java/com/seibel/lod/enums/HorizontalQuality.java new file mode 100644 index 000000000..09472c320 --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/HorizontalQuality.java @@ -0,0 +1,53 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +/** + * Lowest
+ * Low
+ * Medium
+ * High
+ *
+ * this indicates the base of the quadratic function we use for the quality drop off + * + * @author Leonardo Amato + * @version 9-29-2021 + */ +public enum HorizontalQuality +{ + /** 1.0 AKA Linear */ + LOWEST(1.0f), + + /** exponent 1.5 */ + LOW(1.5f), + + /** exponent 2.0 */ + MEDIUM(2.0f), + + /** exponent 2.2 */ + HIGH(2.2f); + + public final double quadraticBase; + + HorizontalQuality(double distanceUnit) + { + this.quadraticBase = distanceUnit; + } +} \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/enums/HorizontalResolution.java b/src/main/java/com/seibel/lod/enums/HorizontalResolution.java new file mode 100644 index 000000000..e8cdbbfa9 --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/HorizontalResolution.java @@ -0,0 +1,175 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +import java.util.ArrayList; +import java.util.Collections; + +import com.seibel.lod.util.LodUtil; + +/** + * chunk
+ * half_chunk
+ * four_blocks
+ * two_blocks
+ * block
+ * + * @author James Seibel + * @author Leonardo Amato + * @version 9-25-2021 + */ +public enum HorizontalResolution +{ + /** render 256 LODs for each chunk */ + BLOCK(16, 0), + + /** render 64 LODs for each chunk */ + TWO_BLOCKS(8, 1), + + /** render 16 LODs for each chunk */ + FOUR_BLOCKS(4, 2), + + /** render 4 LODs for each chunk */ + HALF_CHUNK(2, 3), + + /** render 1 LOD for each chunk */ + CHUNK(1, 4); + + + + + + /** + * How many DataPoints should + * be drawn per side, per LodChunk + */ + public final int dataPointLengthCount; + + /** How wide each LOD DataPoint is */ + public final int dataPointWidth; + + /** + * This is the same as detailLevel in LodQuadTreeNode, + * lowest is 0 highest is 9 + */ + public final byte detailLevel; + + /* Start/End X/Z give the block positions + * for each individual dataPoint in a LodChunk */ + public final int[] startX; + public final int[] startZ; + + public final int[] endX; + public final int[] endZ; + + + /** + * 1st dimension: LodDetail.detailLevel
+ * 2nd dimension: An array of all LodDetails that are less than or
+ * equal to that detailLevel + */ + private static HorizontalResolution[][] lowerDetailArrays; + + + + + HorizontalResolution(int newLengthCount, int newDetailLevel) + { + detailLevel = (byte) newDetailLevel; + dataPointLengthCount = newLengthCount; + dataPointWidth = 16 / dataPointLengthCount; + + startX = new int[dataPointLengthCount * dataPointLengthCount]; + endX = new int[dataPointLengthCount * dataPointLengthCount]; + + startZ = new int[dataPointLengthCount * dataPointLengthCount]; + endZ = new int[dataPointLengthCount * dataPointLengthCount]; + + + int index = 0; + for (int x = 0; x < newLengthCount; x++) + { + for (int z = 0; z < newLengthCount; z++) + { + startX[index] = x * dataPointWidth; + startZ[index] = z * dataPointWidth; + + endX[index] = (x * dataPointWidth) + dataPointWidth; + endZ[index] = (z * dataPointWidth) + dataPointWidth; + + index++; + } + } + + }// constructor + + + + + + + /** + * Returns an array of all LodDetails that have a detail level + * that is less than or equal to the given LodDetail + */ + public static HorizontalResolution[] getSelfAndLowerDetails(HorizontalResolution detail) + { + if (lowerDetailArrays == null) + { + // run first time setup + lowerDetailArrays = new HorizontalResolution[HorizontalResolution.values().length][]; + + // go through each LodDetail + for (HorizontalResolution currentDetail : HorizontalResolution.values()) + { + ArrayList lowerDetails = new ArrayList<>(); + + // find the details lower than currentDetail + for (HorizontalResolution compareDetail : HorizontalResolution.values()) + { + if (currentDetail.detailLevel <= compareDetail.detailLevel) + { + lowerDetails.add(compareDetail); + } + } + + // have the highest detail item first in the list + Collections.sort(lowerDetails); + Collections.reverse(lowerDetails); + + lowerDetailArrays[currentDetail.detailLevel] = lowerDetails.toArray(new HorizontalResolution[lowerDetails.size()]); + } + } + + return lowerDetailArrays[detail.detailLevel]; + } + + /** Returns what detail level should be used at a given distance and maxDistance. */ + public static HorizontalResolution getDetailForDistance(HorizontalResolution maxDetailLevel, int distance, int maxDistance) + { + HorizontalResolution[] lowerDetails = getSelfAndLowerDetails(maxDetailLevel); + int distanceBetweenDetails = maxDistance / lowerDetails.length; + int index = LodUtil.clamp(0, distance / distanceBetweenDetails, lowerDetails.length - 1); + + return lowerDetails[index]; + + } + +} diff --git a/src/main/java/com/seibel/lod/enums/HorizontalScale.java b/src/main/java/com/seibel/lod/enums/HorizontalScale.java new file mode 100644 index 000000000..cf941d1e6 --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/HorizontalScale.java @@ -0,0 +1,49 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +/** + * Low
+ * Medium
+ * High
+ *
+ * this is a quality scale for the detail drop-off + * + * @author Leonardo Amato + * @version 9-25-2021 + */ +public enum HorizontalScale +{ + /** Lods are 2D with heightMap */ + LOW(64), + + /** Lods expand in three dimension */ + MEDIUM(128), + + /** Lods expand in three dimension */ + HIGH(256); + + public final int distanceUnit; + + HorizontalScale(int distanceUnit) + { + this.distanceUnit = distanceUnit; + } +} \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/enums/LodTemplate.java b/src/main/java/com/seibel/lod/enums/LodTemplate.java new file mode 100644 index 000000000..965838571 --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/LodTemplate.java @@ -0,0 +1,62 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +import com.seibel.lod.builders.bufferBuilding.lodTemplates.AbstractLodTemplate; +import com.seibel.lod.builders.bufferBuilding.lodTemplates.CubicLodTemplate; +import com.seibel.lod.builders.bufferBuilding.lodTemplates.DynamicLodTemplate; +import com.seibel.lod.builders.bufferBuilding.lodTemplates.TriangularLodTemplate; + +/** + * Cubic, Triangular, Dynamic + * + * @author James Seibel + * @version 10-10-2021 + */ +public enum LodTemplate +{ + /** + * LODs are rendered as + * rectangular prisms. + */ + CUBIC(new CubicLodTemplate()), + + /** + * LODs smoothly transition between + * each other. + */ + TRIANGULAR(new TriangularLodTemplate()), + + /** + * LODs smoothly transition between + * each other, unless a neighboring LOD + * is at a significantly different height. + */ + DYNAMIC(new DynamicLodTemplate()); + + + public final AbstractLodTemplate template; + + LodTemplate(AbstractLodTemplate newTemplate) + { + template = newTemplate; + } + +} diff --git a/src/main/java/com/seibel/lod/enums/ShadingMode.java b/src/main/java/com/seibel/lod/enums/ShadingMode.java new file mode 100644 index 000000000..89d7d0405 --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/ShadingMode.java @@ -0,0 +1,22 @@ +package com.seibel.lod.enums; + +/** + * NONE, GAME_SHADING + * + * @author James Seibel + * @version 7-25-2020 + */ +public enum ShadingMode +{ + /** + * LODs will have darker sides and bottoms to simulate + * Minecraft's fast lighting. + */ + GAME_SHADING, + + /** + * LODs will use ambient occlusion to mimic Minecarft's + * Fancy lighting. + */ + AMBIENT_OCCLUSION +} \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/enums/VanillaOverdraw.java b/src/main/java/com/seibel/lod/enums/VanillaOverdraw.java new file mode 100644 index 000000000..aa980a6ff --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/VanillaOverdraw.java @@ -0,0 +1,45 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +/** + * None, Dynamic, Always + * + *

+ * This represents how far the LODs should overlap with + * the vanilla Minecraft terrain. + * + * @author James Seibel + * @version 10-11-2021 + */ +public enum VanillaOverdraw +{ + /** Never draw LODs where a minecraft chunk could be. */ + NEVER, + + /** Draw LODs over the farther minecraft chunks. */ + DYNAMIC, + + /** Draw LODs over all minecraft chunks. */ + ALWAYS, + + /** Draw LODs over border chunks. */ + BORDER, +} \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/enums/VerticalQuality.java b/src/main/java/com/seibel/lod/enums/VerticalQuality.java new file mode 100644 index 000000000..b66cff165 --- /dev/null +++ b/src/main/java/com/seibel/lod/enums/VerticalQuality.java @@ -0,0 +1,80 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.enums; + +/** + * heightmap
+ * multi_lod
+ * + * @author Leonardo Amato + * @version 10-07-2021 + */ +public enum VerticalQuality +{ + LOW( + new int[] { 2, + 2, + 2, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1 } + ), + + MEDIUM( + new int[] { 4, + 4, + 2, + 2, + 2, + 1, + 1, + 1, + 1, + 1, + 1 } + ), + + HIGH( + new int[] { + 8, + 8, + 4, + 4, + 2, + 2, + 2, + 1, + 1, + 1, + 1 } + ); + + public final int[] maxVerticalData; + + VerticalQuality(int[] maxVerticalData) + { + this.maxVerticalData = maxVerticalData; + } +} \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/handlers/ChunkLoader.java b/src/main/java/com/seibel/lod/handlers/ChunkLoader.java new file mode 100644 index 000000000..3c1b858ac --- /dev/null +++ b/src/main/java/com/seibel/lod/handlers/ChunkLoader.java @@ -0,0 +1,74 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.handlers; + +import java.io.File; + +import com.seibel.lod.util.LodUtil; +import com.seibel.lod.wrappers.MinecraftWrapper; + +import net.minecraft.client.world.ClientWorld; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.chunk.IChunk; +import net.minecraft.world.chunk.storage.ChunkSerializer; +import net.minecraft.world.server.ServerWorld; + +/** + * + * @author ?? + * @version ?? + */ +public class ChunkLoader +{ + public static IChunk getChunkFromFile(ChunkPos pos){ + + ClientWorld clientWorld = MinecraftWrapper.INSTANCE.getClientWorld(); + if (clientWorld == null) + return null; + ServerWorld serverWorld = LodUtil.getServerWorldFromDimension(clientWorld.dimensionType()); + try + { + File file = new File(serverWorld.getChunkSource().getDataStorage().dataFolder.getParent() + File.separatorChar + "region", "r." + (pos.x >> 5) + "." + (pos.z >> 5) + ".mca"); + if(!file.exists()) + return null; + IChunk loadedChunk = ChunkSerializer.read( + serverWorld, + serverWorld.getStructureManager(), + serverWorld.getPoiManager(), + pos, + serverWorld.getChunkSource().chunkMap.read(pos) + ); + boolean emptyChunk = true; + for(int i = 0; i < 16; i++){ + for(int j = 0; j < 16; j++){ + emptyChunk &= loadedChunk.isYSpaceEmpty(i,j); + } + } + if(emptyChunk) + return null; + else + return loadedChunk; + } + catch (Exception e) + { + return null; + } + } +} diff --git a/src/main/java/com/seibel/lod/handlers/LodDimensionFileHandler.java b/src/main/java/com/seibel/lod/handlers/LodDimensionFileHandler.java new file mode 100644 index 000000000..33a172570 --- /dev/null +++ b/src/main/java/com/seibel/lod/handlers/LodDimensionFileHandler.java @@ -0,0 +1,401 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.handlers; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; +import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream; + +import com.seibel.lod.enums.DistanceGenerationMode; +import com.seibel.lod.enums.VerticalQuality; +import com.seibel.lod.objects.LodDimension; +import com.seibel.lod.objects.LodRegion; +import com.seibel.lod.objects.RegionPos; +import com.seibel.lod.objects.VerticalLevelContainer; +import com.seibel.lod.proxy.ClientProxy; +import com.seibel.lod.util.LodThreadFactory; +import com.seibel.lod.util.LodUtil; +import com.seibel.lod.util.ThreadMapUtil; + +/** + * This object handles creating LodRegions + * from files and saving LodRegion objects + * to file. + * + * @author James Seibel + * @author Cola + * @version 9-25-2021 + */ +public class LodDimensionFileHandler +{ + /** This is the dimension that owns this file handler */ + private LodDimension lodDimension; + + private final File dimensionDataSaveFolder; + + /** lod */ + private static final String FILE_NAME_PREFIX = "lod"; + /** .txt */ + private static final String FILE_EXTENSION = ".xz"; + /** detail- */ + private static final String DETAIL_FOLDER_NAME_PREFIX = "detail-"; + + /** + * .tmp
+ * Added to the end of the file path when saving to prevent + * nulling a currently existing file.
+ * After the file finishes saving it will end with + * FILE_EXTENSION. + */ + private static final String TMP_FILE_EXTENSION = ".tmp"; + + /** + * This is the file version currently accepted by this + * file handler, older versions (smaller numbers) will be deleted and overwritten, + * newer versions (larger numbers) will be ignored and won't be read. + */ + public static final int LOD_SAVE_FILE_VERSION = 6; + + /** + * Allow saving asynchronously, but never try to save multiple regions + * at a time + */ + private final ExecutorService fileWritingThreadPool = Executors.newSingleThreadExecutor(new LodThreadFactory(this.getClass().getSimpleName())); + + + + + public LodDimensionFileHandler(File newSaveFolder, LodDimension newLodDimension) + { + if (newSaveFolder == null) + throw new IllegalArgumentException("LodDimensionFileHandler requires a valid File location to read and write to."); + + dimensionDataSaveFolder = newSaveFolder; + lodDimension = newLodDimension; + } + + + + //================// + // read from file // + //================// + + /** + * Returns the LodRegion at the given coordinates. + * Returns an empty region if the file doesn't exist. + */ + public LodRegion loadRegionFromFile(byte detailLevel, RegionPos regionPos, DistanceGenerationMode generationMode, VerticalQuality verticalQuality) + { + int regionX = regionPos.x; + int regionZ = regionPos.z; + LodRegion region = new LodRegion(LodUtil.REGION_DETAIL_LEVEL, regionPos, generationMode, verticalQuality); + + for (byte tempDetailLevel = LodUtil.REGION_DETAIL_LEVEL; tempDetailLevel >= detailLevel; tempDetailLevel--) + { + String fileName = getFileNameAndPathForRegion(regionX, regionZ, generationMode, tempDetailLevel, verticalQuality); + + try + { + // if the fileName was null that means the folder is inaccessible + // for some reason + if (fileName == null) + throw new IllegalArgumentException("Unable to read region [" + regionX + ", " + regionZ + "] file, no fileName."); + + File file = new File(fileName); + if (!file.exists()) + { + //there is no file for current gen mode + //search others above current from the most to the least detailed + DistanceGenerationMode tempGenMode = DistanceGenerationMode.SERVER; + while (tempGenMode != generationMode) + { + fileName = getFileNameAndPathForRegion(regionX, regionZ, tempGenMode, tempDetailLevel, verticalQuality); + if (fileName != null) + { + file = new File(fileName); + if (file.exists()) + break; + } + //decrease gen mode + if (tempGenMode == DistanceGenerationMode.SERVER) + tempGenMode = DistanceGenerationMode.FEATURES; + else if (tempGenMode == DistanceGenerationMode.FEATURES) + tempGenMode = DistanceGenerationMode.SURFACE; + else if (tempGenMode == DistanceGenerationMode.SURFACE) + tempGenMode = DistanceGenerationMode.BIOME_ONLY_SIMULATE_HEIGHT; + else if (tempGenMode == DistanceGenerationMode.BIOME_ONLY_SIMULATE_HEIGHT) + tempGenMode = DistanceGenerationMode.BIOME_ONLY; + else if (tempGenMode == DistanceGenerationMode.BIOME_ONLY) + tempGenMode = DistanceGenerationMode.NONE; + } + if (!file.exists()) + //there wasn't a file, don't return anything + continue; + } + + + + // don't try parsing empty files + long dataSize = file.length(); + dataSize -= 1; + if (dataSize > 0) + { + try (XZCompressorInputStream inputStream = new XZCompressorInputStream(new FileInputStream(file))) + { + int fileVersion; + fileVersion = inputStream.read(); + + // check if this file can be read by this file handler + if (fileVersion < LOD_SAVE_FILE_VERSION) + { + // the file we are reading is an older version, + // close the reader and delete the file. + inputStream.close(); + file.delete(); + ClientProxy.LOGGER.info("Outdated LOD region file for region: (" + regionX + "," + regionZ + ")" + + " version found: " + fileVersion + + ", version requested: " + LOD_SAVE_FILE_VERSION + + ". File was been deleted."); + + break; + } + else if (fileVersion > LOD_SAVE_FILE_VERSION) + { + // the file we are reading is a newer version, + // close the reader and ignore the file, we don't + // want to accidentally delete anything the user may want. + inputStream.close(); + ClientProxy.LOGGER.info("Newer LOD region file for region: (" + regionX + "," + regionZ + ")" + + " version found: " + fileVersion + + ", version requested: " + LOD_SAVE_FILE_VERSION + + " this region will not be written to in order to protect the newer file."); + + break; + } + + + // this file is a readable version, + // read the file + byte[] data = ThreadMapUtil.getSaveContainer(tempDetailLevel); + inputStream.read(data); + inputStream.close(); + + + // add the data to our region + region.addLevelContainer(new VerticalLevelContainer(data)); + } + catch (IOException ioEx) + { + ClientProxy.LOGGER.error("LOD file read error. Unable to read to [" + fileName + "] error [" + ioEx.getMessage() + "]: "); + ioEx.printStackTrace(); + } + } + } + catch (Exception e) + { + // the buffered reader encountered a + // problem reading the file + ClientProxy.LOGGER.error("LOD file read error. Unable to read to [" + fileName + "] error [" + e.getMessage() + "]: "); + e.printStackTrace(); + } + }// for each detail level + + if (region.getMinDetailLevel() >= detailLevel) + region.growTree(detailLevel); + + return region; + } + + + //==============// + // Save to File // + //==============// + + /** + * Save all dirty regions in this LodDimension to file. + */ + public void saveDirtyRegionsToFileAsync() + { + fileWritingThreadPool.execute(saveDirtyRegionsThread); + } + + private final Thread saveDirtyRegionsThread = new Thread(() -> + { + try + { + for (int i = 0; i < lodDimension.getWidth(); i++) + { + for (int j = 0; j < lodDimension.getWidth(); j++) + { + if (lodDimension.GetIsRegionDirty(i, j) && lodDimension.getRegionByArrayIndex(i, j) != null) + { + saveRegionToFile(lodDimension.getRegionByArrayIndex(i, j)); + lodDimension.SetIsRegionDirty(i, j, false); + } + } + } + } + catch (Exception e) + { + e.printStackTrace(); + } + }); + + /** + * Save a specific region to disk.
+ * Note:
+ * 1. If a file already exists for a newer version + * the file won't be written.
+ * 2. This will save to the LodDimension that this + * handler is associated with. + */ + private void saveRegionToFile(LodRegion region) + { + for (byte detailLevel = region.getMinDetailLevel(); detailLevel <= LodUtil.REGION_DETAIL_LEVEL; detailLevel++) + { + String fileName = getFileNameAndPathForRegion(region.regionPosX, region.regionPosZ, region.getGenerationMode(), detailLevel, region.getVerticalQuality()); + + // if the fileName was null that means the folder is inaccessible + // for some reason + if (fileName == null) + { + ClientProxy.LOGGER.warn("Unable to save region [" + region.regionPosX + ", " + region.regionPosZ + "] to file, file is inaccessible."); + return; + } + File oldFile = new File(fileName); + //ClientProxy.LOGGER.info("saving region [" + region.regionPosX + ", " + region.regionPosZ + "] to file."); + + try + { + // make sure the file and folder exists + if (!oldFile.exists()) + { + // the file doesn't exist, + // create it and the folder if need be + if (!oldFile.getParentFile().exists()) + oldFile.getParentFile().mkdirs(); + oldFile.createNewFile(); + } + else + { + // the file exists, make sure it + // is the correct version. + // (to make sure we don't overwrite a newer + // version file if it exists) + int fileVersion = LOD_SAVE_FILE_VERSION; + try (XZCompressorInputStream inputStream = new XZCompressorInputStream(new FileInputStream(oldFile))) + { + fileVersion = inputStream.read(); + inputStream.close(); + } + catch (IOException ex) + { + ex.printStackTrace(); + } + + // check if this file can be written to by the file handler + if (fileVersion > LOD_SAVE_FILE_VERSION) + { + // the file we are reading is a newer version, + // don't write anything, we don't want to accidentally + // delete anything the user may want. + return; + } + + // if we got this far then we are good + // to overwrite the old file + } + + // the old file is good, now create a new temporary save file + File newFile = new File(fileName + TMP_FILE_EXTENSION); + try (XZCompressorOutputStream outputStream = new XZCompressorOutputStream(new FileOutputStream(newFile), 3)) + { + // add the version of this file + outputStream.write(LOD_SAVE_FILE_VERSION); + + // add each LodChunk to the file + outputStream.write(region.getLevel(detailLevel).toDataString()); + outputStream.close(); + + // overwrite the old file with the new one + Files.move(newFile.toPath(), oldFile.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } + catch (IOException ex) + { + ex.printStackTrace(); + } + } + catch (Exception e) + { + ClientProxy.LOGGER.error("LOD file write error. Unable to write to [" + fileName + "] error [" + e.getMessage() + "]: "); + e.printStackTrace(); + } + } + } + + + + + + //================// + // helper methods // + //================// + + + /** + * Return the name of the file that should contain the + * region at the given x and z.
+ * Returns null if this object isn't available to read and write.

+ *

+ * example: "lod.0.0.txt"
+ *

+ * Returns null if there is an IO or security Exception. + */ + private String getFileNameAndPathForRegion(int regionX, int regionZ, DistanceGenerationMode generationMode, byte detailLevel, VerticalQuality verticalQuality) + { + try + { + // saveFolder is something like + // ".\Super Flat\DIM-1\data\" + // or + // ".\Super Flat\data\" + return dimensionDataSaveFolder.getCanonicalPath() + File.separatorChar + + verticalQuality + File.separatorChar + + generationMode.toString() + File.separatorChar + + DETAIL_FOLDER_NAME_PREFIX + detailLevel + File.separatorChar + + FILE_NAME_PREFIX + "." + regionX + "." + regionZ + FILE_EXTENSION; + } + catch (IOException | SecurityException e) + { + ClientProxy.LOGGER.warn("Unable to get the filename for the region [" + regionX + ", " + regionZ + "], error: [" + e.getMessage() + "], stacktrace: "); + e.printStackTrace(); + return null; + } + } + +} diff --git a/src/main/java/com/seibel/lod/handlers/ReflectionHandler.java b/src/main/java/com/seibel/lod/handlers/ReflectionHandler.java new file mode 100644 index 000000000..1eb0a5bdb --- /dev/null +++ b/src/main/java/com/seibel/lod/handlers/ReflectionHandler.java @@ -0,0 +1,165 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.handlers; + +import com.seibel.lod.enums.FogQuality; +import com.seibel.lod.proxy.ClientProxy; +import com.seibel.lod.wrappers.MinecraftWrapper; +import net.minecraft.util.math.vector.Matrix4f; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * This object is used to get variables from methods + * where they are private. Specifically the fog setting + * in Optifine. + * @author James Seibel + * @version 9 -25-2021 + */ +public class ReflectionHandler +{ + public static final ReflectionHandler INSTANCE = new ReflectionHandler(); + private final MinecraftWrapper mc = MinecraftWrapper.INSTANCE; + + public Field ofFogField = null; + public Method vertexBufferUploadMethod = null; + + + + private ReflectionHandler() + { + setupFogField(); + } + + + /** + * finds the Optifine fog type field + */ + private void setupFogField() + { + // get every variable from the entity renderer + Field[] optionFields = mc.getOptions().getClass().getDeclaredFields(); + + // try and find the ofFogType variable in gameSettings + for (Field field : optionFields) + { + if (field.getName().equals("ofFogType")) + { + ofFogField = field; + return; + } + } + + // we didn't find the field, + // either optifine isn't installed, or + // optifine changed the name of the variable + ClientProxy.LOGGER.info(ReflectionHandler.class.getSimpleName() + ": unable to find the Optifine fog field. If Optifine isn't installed this can be ignored."); + } + + + /** + * Get what type of fog optifine is currently set to render. + * @return the fog quality + */ + public FogQuality getFogQuality() + { + if (ofFogField == null) + { + // either optifine isn't installed, + // the variable name was changed, or + // the setup method wasn't called yet. + return FogQuality.FANCY; + } + + int returnNum = 0; + + try + { + returnNum = (int) ofFogField.get(mc.getOptions()); + } + catch (IllegalArgumentException | IllegalAccessException e) + { + e.printStackTrace(); + } + + switch (returnNum) + { + default: + case 0: + // optifine's "default" option, + // it should never be called in this case + + // normal options + case 1: + return FogQuality.FAST; + case 2: + return FogQuality.FANCY; + case 3: + return FogQuality.OFF; + } + } + + /** Detect if Vivecraft is present using reflection. Attempts to find the "VRRenderer" class. */ + public boolean detectVivecraft() + { + try + { + Class.forName("org.vivecraft.provider.VRRenderer"); + return true; + } + catch (ClassNotFoundException ignored) + { + System.out.println("Vivecraft not detected."); + } + return false; + } + + /** + * Modifies a projection matrix's clip planes. + * The projection matrix must be in a column-major format. + * @param projectionMatrix The projection matrix to be modified. + * @param n the new near clip plane value. + * @param f the new far clip plane value. + * @return The modified matrix. + */ + public Matrix4f Matrix4fModifyClipPlanes(Matrix4f projectionMatrix, float n, float f) + { + //find the matrix values. + float nMatrixValue = -((f + n) / (f - n)); + float fMatrixValue = -((2 * f * n) / (f - n)); + try + { + //get the fields of the projectionMatrix + Field[] fields = projectionMatrix.getClass().getDeclaredFields(); + //bypass the security protections on the fields that encode near and far plane values. + fields[10].setAccessible(true); + fields[11].setAccessible(true); + //Change the values of the near and far plane. + fields[10].set(projectionMatrix, nMatrixValue); + fields[11].set(projectionMatrix, fMatrixValue); + } + catch (Exception e) + { + e.printStackTrace(); + } + return projectionMatrix; + } +} diff --git a/src/main/java/com/seibel/lod/mixin/MixinWorldRenderer.java b/src/main/java/com/seibel/lod/mixin/MixinWorldRenderer.java new file mode 100644 index 000000000..23ba25de0 --- /dev/null +++ b/src/main/java/com/seibel/lod/mixin/MixinWorldRenderer.java @@ -0,0 +1,64 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.mojang.blaze3d.matrix.MatrixStack; +import com.seibel.lod.LodMain; + +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.WorldRenderer; + +/** + * This class is used to mix in my rendering code + * before Minecraft starts rendering blocks. + * If this wasn't done, and we used Forge's + * render last event, the LODs would render on top + * of the normal terrain. + * + * @author James Seibel + * @version 9-19-2021 + */ +@Mixin(WorldRenderer.class) +public class MixinWorldRenderer +{ + private static float previousPartialTicks = 0; + + @Inject(at = @At("RETURN"), method = "renderSky(Lcom/mojang/blaze3d/matrix/MatrixStack;F)V") + private void renderSky(MatrixStack matrixStackIn, float partialTicks, CallbackInfo callback) + { + // get the partial ticks since renderBlockLayer doesn't + // have access to them + previousPartialTicks = partialTicks; + } + + @Inject(at = @At("HEAD"), method = "renderChunkLayer(Lnet/minecraft/client/renderer/RenderType;Lcom/mojang/blaze3d/matrix/MatrixStack;DDD)V") + private void renderChunkLayer(RenderType renderType, MatrixStack matrixStackIn, double xIn, double yIn, double zIn, CallbackInfo callback) + { + // only render if LODs are enabled and + // only render before solid blocks + if (renderType.equals(RenderType.solid())) + LodMain.client_proxy.renderLods(matrixStackIn, previousPartialTicks); + } +} diff --git a/src/main/java/com/seibel/lod/objects/LevelContainer.java b/src/main/java/com/seibel/lod/objects/LevelContainer.java new file mode 100644 index 000000000..74692f950 --- /dev/null +++ b/src/main/java/com/seibel/lod/objects/LevelContainer.java @@ -0,0 +1,115 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.objects; + +/** + * A level container is a quad tree level + */ +public interface LevelContainer +{ + /** + * With this you can add data to the level container + * @param data actual data to add in an array of long format. + * @param posX x position in the detail level + * @param posZ z position in the detail level + * @param index z position in the detail level + * @return true if correctly added, false otherwise + */ + boolean addData(long data, int posX, int posZ, int index); + + /** + * With this you can add data to the level container + * @param data actual data to add in an array of long[] format. + * @param posX x position in the detail level + * @param posZ z position in the detail level + * @return true if correctly added, false otherwise + */ + boolean addVerticalData(long[] data, int posX, int posZ); + + /** + * With this you can add data to the level container + * @param data actual data to add in an array of long format. + * @param posX x position in the detail level + * @param posZ z position in the detail level + * @return true if correctly added, false otherwise + */ + boolean addSingleData(long data, int posX, int posZ); + + /** + * With this you can get data from the level container + * @param posX x position in the detail level + * @param posZ z position in the detail level + * @return the data in long array format + */ + long getData(int posX, int posZ, int index); + + /** + * With this you can get data from the level container + * @param posX x position in the detail level + * @param posZ z position in the detail level + * @return the data in long array format + */ + long getSingleData(int posX, int posZ); + + /** + * @param posX x position in the detail level + * @param posZ z position in the detail level + * @return true only if the data exist + */ + boolean doesItExist(int posX, int posZ); + + /** + * @return return the detailLevel of this level container + */ + byte getDetailLevel(); + + + int getMaxVerticalData(); + + /** Clears the dataPoint at the given array index */ + void clear(int posX, int posZ); + + /** + * This return a level container with detail level lower than the current level. + * The new level container may use information of this level. + * @return the new level container + */ + LevelContainer expand(); + + /** + * @param lowerLevelContainer lower level where we extract the data + * @param posX x position in the detail level to update + * @param posZ z position in the detail level to update + */ + void updateData(LevelContainer lowerLevelContainer, int posX, int posZ); + + /** + * This will give the data to save in the file + * @return data as a String + */ + byte[] toDataString(); + + + /** + * This will give the data to save in the file + * @return data as a String + */ + int getMaxNumberOfLods(); +} diff --git a/src/main/java/com/seibel/lod/objects/LodDimension.java b/src/main/java/com/seibel/lod/objects/LodDimension.java new file mode 100644 index 000000000..e6708ba4f --- /dev/null +++ b/src/main/java/com/seibel/lod/objects/LodDimension.java @@ -0,0 +1,905 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.objects; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.seibel.lod.config.LodConfig; +import com.seibel.lod.enums.DistanceGenerationMode; +import com.seibel.lod.enums.GenerationPriority; +import com.seibel.lod.enums.VerticalQuality; +import com.seibel.lod.handlers.LodDimensionFileHandler; +import com.seibel.lod.util.DataPointUtil; +import com.seibel.lod.util.DetailDistanceUtil; +import com.seibel.lod.util.LevelPosUtil; +import com.seibel.lod.util.LodThreadFactory; +import com.seibel.lod.util.LodUtil; +import com.seibel.lod.wrappers.MinecraftWrapper; + +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.DimensionType; +import net.minecraft.world.server.ServerChunkProvider; +import net.minecraft.world.server.ServerWorld; + + +/** + * This object holds all loaded LOD regions + * for a given dimension.

+ * + * Coordinate Standard:
+ * Coordinate called posX or posZ are relative LevelPos coordinates
+ * unless stated otherwise.
+ * + * @author Leonardo Amato + * @author James Seibel + * @version 10-10-2021 + */ +public class LodDimension +{ + public final DimensionType dimension; + + /** measured in regions */ + private volatile int width; + /** measured in regions */ + private volatile int halfWidth; + + // these three variables are private to force use of the getWidth() method + // which is a safer way to get the width then directly asking the arrays + /** stores all the regions in this dimension */ + public volatile LodRegion[][] regions; + + /** stores if the region at the given x and z index needs to be saved to disk */ + private volatile boolean[][] isRegionDirty; + /** stores if the region at the given x and z index needs to be regenerated */ + private volatile boolean[][] regenRegionBuffer; + /** stores if the buffer size at the given x and z index needs to be changed */ + private volatile boolean[][] recreateRegionBuffer; + + /** + * if true that means there are regions in this dimension + * that need to have their buffers rebuilt. + */ + public volatile boolean regenDimensionBuffers = false; + + private LodDimensionFileHandler fileHandler; + + private final RegionPos center; + + /** prevents the cutAndExpandThread from expanding at the same location multiple times */ + private volatile ChunkPos lastExpandedChunk; + /** prevents the cutAndExpandThread from cutting at the same location multiple times */ + private volatile ChunkPos lastCutChunk; + private final ExecutorService cutAndExpandThread = Executors.newSingleThreadExecutor(new LodThreadFactory(this.getClass().getSimpleName() + " - Cut and Expand")); + + /** + * Creates the dimension centered at (0,0) + * @param newWidth in regions + */ + public LodDimension(DimensionType newDimension, LodWorld lodWorld, int newWidth) + { + lastCutChunk = null; + lastExpandedChunk = null; + dimension = newDimension; + width = newWidth; + halfWidth = width / 2; + MinecraftWrapper mc = MinecraftWrapper.INSTANCE; + + if (newDimension != null && lodWorld != null) + { + try + { + File saveDir; + if (mc.hasSinglePlayerServer()) + { + // local world + + ServerWorld serverWorld = LodUtil.getServerWorldFromDimension(newDimension); + + // provider needs a separate variable to prevent + // the compiler from complaining + ServerChunkProvider provider = serverWorld.getChunkSource(); + saveDir = new File(provider.dataStorage.dataFolder.getCanonicalFile().getPath() + File.separatorChar + "lod"); + } + else + { + // connected to server + + saveDir = new File(mc.getGameDirectory().getCanonicalFile().getPath() + + File.separatorChar + "lod server data" + File.separatorChar + mc.getCurrentDimensionId()); + } + + fileHandler = new LodDimensionFileHandler(saveDir, this); + } + catch (IOException e) + { + // the file handler wasn't able to be created + // we won't be able to read or write any files + } + } + + + regions = new LodRegion[width][width]; + isRegionDirty = new boolean[width][width]; + regenRegionBuffer = new boolean[width][width]; + recreateRegionBuffer = new boolean[width][width]; + + center = new RegionPos(0, 0); + } + + + /** + * Move the center of this LodDimension and move all owned + * regions over by the given x and z offset.

+ *

+ * Synchronized to prevent multiple moves happening on top of each other. + */ + public synchronized void move(RegionPos regionOffset) + { + int xOffset = regionOffset.x; + int zOffset = regionOffset.z; + + // if the x or z offset is equal to or greater than + // the total width, just delete the current data + // and update the centerX and/or centerZ + if (Math.abs(xOffset) >= width || Math.abs(zOffset) >= width) + { + for (int x = 0; x < width; x++) + for (int z = 0; z < width; z++) + regions[x][z] = null; + + // update the new center + center.x += xOffset; + center.z += zOffset; + + return; + } + + + // X + if (xOffset > 0) + { + // move everything over to the left (as the center moves to the right) + for (int x = 0; x < width; x++) + { + for (int z = 0; z < width; z++) + { + if (x + xOffset < width) + regions[x][z] = regions[x + xOffset][z]; + else + regions[x][z] = null; + } + } + } + else + { + // move everything over to the right (as the center moves to the left) + for (int x = width - 1; x >= 0; x--) + { + for (int z = 0; z < width; z++) + { + if (x + xOffset >= 0) + regions[x][z] = regions[x + xOffset][z]; + else + regions[x][z] = null; + } + } + } + + + // Z + if (zOffset > 0) + { + // move everything up (as the center moves down) + for (int x = 0; x < width; x++) + { + for (int z = 0; z < width; z++) + { + if (z + zOffset < width) + regions[x][z] = regions[x][z + zOffset]; + else + regions[x][z] = null; + } + } + } + else + { + // move everything down (as the center moves up) + for (int x = 0; x < width; x++) + { + for (int z = width - 1; z >= 0; z--) + { + if (z + zOffset >= 0) + regions[x][z] = regions[x][z + zOffset]; + else + regions[x][z] = null; + } + } + } + + + // update the new center + center.x += xOffset; + center.z += zOffset; + } + + + /** + * Gets the region at the given LevelPos + *
+ * Returns null if the region doesn't exist + * or is outside the loaded area. + */ + public LodRegion getRegion(byte detailLevel, int levelPosX, int levelPosZ) + { + int xRegion = LevelPosUtil.getRegion(detailLevel, levelPosX); + int zRegion = LevelPosUtil.getRegion(detailLevel, levelPosZ); + int xIndex = (xRegion - center.x) + halfWidth; + int zIndex = (zRegion - center.z) + halfWidth; + + if (!regionIsInRange(xRegion, zRegion)) + return null; + // throw new ArrayIndexOutOfBoundsException("Region for level pos " + LevelPosUtil.toString(detailLevel, posX, posZ) + " out of range"); + else if (regions[xIndex][zIndex] == null) + return null; + else if (regions[xIndex][zIndex].getMinDetailLevel() > detailLevel) + return null; + //throw new InvalidParameterException("Region for level pos " + LevelPosUtil.toString(detailLevel, posX, posZ) + " currently only reach level " + regions[xIndex][zIndex].getMinDetailLevel()); + + return regions[xIndex][zIndex]; + } + + /** + * Gets the region at the given X and Z + *
+ * Returns null if the region doesn't exist + * or is outside the loaded area. + */ + public LodRegion getRegion(int regionPosX, int regionPosZ) + { + int xIndex = (regionPosX - center.x) + halfWidth; + int zIndex = (regionPosZ - center.z) + halfWidth; + + if (!regionIsInRange(regionPosX, regionPosZ)) + return null; + //throw new ArrayIndexOutOfBoundsException("Region " + regionPosX + " " + regionPosZ + " out of range"); + + return regions[xIndex][zIndex]; + } + + /** Useful when iterating over every region. */ + public LodRegion getRegionByArrayIndex(int xIndex, int zIndex) + { + return regions[xIndex][zIndex]; + } + + /** + * Overwrite the LodRegion at the location of newRegion with newRegion. + * @throws ArrayIndexOutOfBoundsException if newRegion is outside what can be stored in this LodDimension. + */ + public synchronized void addOrOverwriteRegion(LodRegion newRegion) throws ArrayIndexOutOfBoundsException + { + int xIndex = (newRegion.regionPosX - center.x) + halfWidth; + int zIndex = (newRegion.regionPosZ - center.z) + halfWidth; + + if (!regionIsInRange(newRegion.regionPosX, newRegion.regionPosZ)) + // out of range + throw new ArrayIndexOutOfBoundsException("Region " + newRegion.regionPosX + ", " + newRegion.regionPosZ + " out of range"); + + regions[xIndex][zIndex] = newRegion; + } + + + /** + * Deletes nodes that are a higher detail then necessary, freeing + * up memory. + */ + public void cutRegionNodesAsync(int playerPosX, int playerPosZ) + { + ChunkPos newPlayerChunk = new ChunkPos(LevelPosUtil.getChunkPos((byte) 0, playerPosX), LevelPosUtil.getChunkPos((byte) 0, playerPosZ)); + + if (lastCutChunk == null) + lastCutChunk = new ChunkPos(newPlayerChunk.x + 1, newPlayerChunk.z - 1); + + // don't run the tree cutter multiple times + // for the same location + if (newPlayerChunk.x != lastCutChunk.x || newPlayerChunk.z != lastCutChunk.z) + { + lastCutChunk = newPlayerChunk; + + Thread thread = new Thread(() -> + { + int regionX; + int regionZ; + int minDistance; + byte detail; + byte minAllowedDetailLevel; + + // go over every region in the dimension + for (int x = 0; x < regions.length; x++) + { + for (int z = 0; z < regions.length; z++) + { + regionX = (x + center.x) - halfWidth; + regionZ = (z + center.z) - halfWidth; + + if (regions[x][z] != null) + { + // check what detail level this region should be + // and cut it if it is higher then that + minDistance = LevelPosUtil.minDistance(LodUtil.REGION_DETAIL_LEVEL, regionX, regionZ, playerPosX, playerPosZ); + detail = DetailDistanceUtil.getTreeCutDetailFromDistance(minDistance); + minAllowedDetailLevel = DetailDistanceUtil.getCutLodDetail(detail); + + if (regions[x][z].getMinDetailLevel() > minAllowedDetailLevel) + { + regions[x][z].cutTree(minAllowedDetailLevel); + recreateRegionBuffer[x][z] = true; + } + } + }// region z + }// region z + }); + + cutAndExpandThread.execute(thread); + } + } + + /** Either expands or loads all regions in the rendered LOD area */ + public void expandOrLoadRegionsAsync(int playerPosX, int playerPosZ) + { + DistanceGenerationMode generationMode = LodConfig.CLIENT.worldGenerator.distanceGenerationMode.get(); + ChunkPos newPlayerChunk = new ChunkPos(LevelPosUtil.getChunkPos((byte) 0, playerPosX), LevelPosUtil.getChunkPos((byte) 0, playerPosZ)); + VerticalQuality verticalQuality = LodConfig.CLIENT.graphics.qualityOption.verticalQuality.get(); + + + if (lastExpandedChunk == null) + lastExpandedChunk = new ChunkPos(newPlayerChunk.x + 1, newPlayerChunk.z - 1); + + // don't run the expander multiple times + // for the same location + if (newPlayerChunk.x != lastExpandedChunk.x || newPlayerChunk.z != lastExpandedChunk.z) + { + lastExpandedChunk = newPlayerChunk; + + Thread thread = new Thread(() -> + { + int regionX; + int regionZ; + LodRegion region; + int minDistance; + byte detail; + byte levelToGen; + + for (int x = 0; x < regions.length; x++) + { + for (int z = 0; z < regions.length; z++) + { + regionX = (x + center.x) - halfWidth; + regionZ = (z + center.z) - halfWidth; + final RegionPos regionPos = new RegionPos(regionX, regionZ); + region = regions[x][z]; + + minDistance = LevelPosUtil.minDistance(LodUtil.REGION_DETAIL_LEVEL, regionX, regionZ, playerPosX, playerPosZ); + detail = DetailDistanceUtil.getTreeGenDetailFromDistance(minDistance); + levelToGen = DetailDistanceUtil.getLodGenDetail(detail).detailLevel; + + // check that the region isn't null and at least this detail level + if (region == null || region.getGenerationMode() != generationMode) + { + // First case, region has to be created + + // try to get the region from file + regions[x][z] = getRegionFromFile(regionPos, levelToGen, generationMode, verticalQuality); + + // if there is no region file create an empty region + if (regions[x][z] == null) + regions[x][z] = new LodRegion(levelToGen, regionPos, generationMode, verticalQuality); + + regenRegionBuffer[x][z] = true; + regenDimensionBuffers = true; + recreateRegionBuffer[x][z] = true; + } + else if (region.getMinDetailLevel() > levelToGen) + { + // Second case, the region exists at a higher detail level. + + // Expand the region by introducing the missing layer + region.growTree(levelToGen); + regions[x][z] = getRegionFromFile(regionPos, levelToGen, generationMode, verticalQuality); + recreateRegionBuffer[x][z] = true; + } + } + } + }); + + cutAndExpandThread.execute(thread); + } + } + + /** + * Use addVerticalData when possible. + * Add the given LOD to this dimension at the coordinate + * stored in the LOD. If an LOD already exists at the given + * coordinate it will be overwritten. + */ + public Boolean addData(byte detailLevel, int posX, int posZ, int verticalIndex, long data, boolean dontSave) + { + int regionPosX = LevelPosUtil.getRegion(detailLevel, posX); + int regionPosZ = LevelPosUtil.getRegion(detailLevel, posZ); + + // don't continue if the region can't be saved + LodRegion region = getRegion(regionPosX, regionPosZ); + if (region == null) + return false; + + boolean nodeAdded = region.addData(detailLevel, posX, posZ, verticalIndex, data); + + // only save valid LODs to disk + if (!dontSave && fileHandler != null) + { + try + { + // mark the region as dirty, so it will be saved to disk + int xIndex = (regionPosX - center.x) + halfWidth; + int zIndex = (regionPosZ - center.z) + halfWidth; + + isRegionDirty[xIndex][zIndex] = true; + regenRegionBuffer[xIndex][zIndex] = true; + regenDimensionBuffers = true; + } + catch (ArrayIndexOutOfBoundsException e) + { + e.printStackTrace(); + // If this happens, the method was probably + // called when the dimension was changing size. + // Hopefully this shouldn't be an issue. + } + } + + return nodeAdded; + } + + /** + * Add whole column of LODs to this dimension at the coordinate + * stored in the LOD. If an LOD already exists at the given + * coordinate it will be overwritten. + */ + public Boolean addVerticalData(byte detailLevel, int posX, int posZ, long[] data, boolean dontSave) + { + int regionPosX = LevelPosUtil.getRegion(detailLevel, posX); + int regionPosZ = LevelPosUtil.getRegion(detailLevel, posZ); + + // don't continue if the region can't be saved + LodRegion region = getRegion(regionPosX, regionPosZ); + if (region == null) + return false; + + boolean nodeAdded = region.addVerticalData(detailLevel, posX, posZ, data); + + // only save valid LODs to disk + if (!dontSave && fileHandler != null) + { + try + { + // mark the region as dirty, so it will be saved to disk + int xIndex = (regionPosX - center.x) + halfWidth; + int zIndex = (regionPosZ - center.z) + halfWidth; + + isRegionDirty[xIndex][zIndex] = true; + regenRegionBuffer[xIndex][zIndex] = true; + regenDimensionBuffers = true; + } + catch (ArrayIndexOutOfBoundsException e) + { + e.printStackTrace(); + // If this happens, the method was probably + // called when the dimension was changing size. + // Hopefully this shouldn't be an issue. + } + } + + return nodeAdded; + } + + /** marks the region at the given region position to have its buffer rebuilt */ + public void markRegionBufferToRegen(int xRegion, int zRegion) + { + int xIndex = (xRegion - center.x) + halfWidth; + int zIndex = (zRegion - center.z) + halfWidth; + regenRegionBuffer[xIndex][zIndex] = true; + } + + /** + * Returns every position that need to be generated based on the position of the player + */ + public PosToGenerateContainer getPosToGenerate(int maxDataToGenerate, int playerBlockPosX, int playerBlockPosZ) + { + PosToGenerateContainer posToGenerate; + LodRegion lodRegion; + // all the following values are used for the spiral matrix visit + // x and z are the matrix coord + // dx and dz is the next move on the coordinate in the range -1 0 +1 + int x, z, dx, dz, t; + x = 0; + z = 0; + dx = 0; + dz = -1; + + // We can use two type of generation scheduling + switch (LodConfig.CLIENT.worldGenerator.generationPriority.get()) + { + default: + case NEAR_FIRST: + //in the NEAR_FIRST generation scheduling we prioritize the nearest un-generated position to the player + //the chunk position to generate will be stored in a posToGenerate object + posToGenerate = new PosToGenerateContainer((byte) 10, maxDataToGenerate, playerBlockPosX, playerBlockPosZ); + + int playerChunkX = LevelPosUtil.getChunkPos(LodUtil.BLOCK_DETAIL_LEVEL, playerBlockPosX); + int playerChunkZ = LevelPosUtil.getChunkPos(LodUtil.BLOCK_DETAIL_LEVEL, playerBlockPosZ); + + int complexity; + int xChunkToCheck; + int zChunkToCheck; + byte detailLevel; + int posX; + int posZ; + long data; + int numbChunksWide = (width) * 32; + int circleLimit = Integer.MAX_VALUE; + + //posToGenerate is using an insertion sort algorithm which can become really fast if the + //original data order is almost ordered. For this reason we explore the matrix of the position to generate + //with a spiral matrix visit (a square spiral is almost ordered in the "nearest to farthest" order) + for (int i = 0; i < numbChunksWide * numbChunksWide; i++) + { + //Firstly we check if the posToGenerate has been filled + if (maxDataToGenerate == 0) + { + maxDataToGenerate--; + //if it has been filled then we set a stop distance + //the stop distance will be current distance (generically x) per square root of 2 + //this would guarantee a circular generation since (Math.abs(x) * 1.41f) is the + //radius of a circle that inscribe a square + circleLimit = (int) (Math.abs(x) * 1.41f); + } + //This second if check if we reached the circleLimit decided in the previous if + //if so we stop + else if (maxDataToGenerate < 0) + { + if (circleLimit < Math.abs(x) && circleLimit < Math.abs(z)) + break; + } + + + xChunkToCheck = x + playerChunkX; + zChunkToCheck = z + playerChunkZ; + + //we get the lod region in which the chunk is present + lodRegion = getRegion(LodUtil.CHUNK_DETAIL_LEVEL, xChunkToCheck, zChunkToCheck); + if (lodRegion == null) + continue; + + //Now we check if the current chunk has been generated with the correct complexity + //if(lodRegion.isChunkPreGenerated(xChunkToCheck,zChunkToCheck)) + // complexity = DistanceGenerationMode.SERVER.complexity; + //else + complexity = LodConfig.CLIENT.worldGenerator.distanceGenerationMode.get().complexity; + + + //we create the level position info of the chunk + detailLevel = lodRegion.getMinDetailLevel(); + posX = LevelPosUtil.convert(LodUtil.CHUNK_DETAIL_LEVEL, xChunkToCheck, detailLevel); + posZ = LevelPosUtil.convert(LodUtil.CHUNK_DETAIL_LEVEL, zChunkToCheck, detailLevel); + + data = getSingleData(detailLevel, posX, posZ); + + //we will generate the position only if the current generation complexity is lower than the target one. + //an un-generated area will always have 0 generation + if (DataPointUtil.getGenerationMode(data) < complexity) + { + posToGenerate.addPosToGenerate(detailLevel, posX, posZ); + if (maxDataToGenerate >= 0) + maxDataToGenerate--; + } + + //with this code section we find the next chunk to check + if ((x == z) || ((x < 0) && (x == -z)) || ((x > 0) && (x == 1 - z))) + { + t = dx; + dx = -dz; + dz = t; + } + x += dx; + z += dz; + } + break; + + + case FAR_FIRST: + //in the FAR_FIRST generation we dedicate part of the generation process to the far region with really + //low detail quality. + + posToGenerate = new PosToGenerateContainer((byte) 8, maxDataToGenerate, playerBlockPosX, playerBlockPosZ); + + int xRegion; + int zRegion; + + for (int i = 0; i < width * width; i++) + { + xRegion = x + center.x; + zRegion = z + center.z; + + //All of this is handled directly by the region, which scan every pos from top to bottom of the quad tree + lodRegion = getRegion(xRegion, zRegion); + if (lodRegion != null) + lodRegion.getPosToGenerate(posToGenerate, playerBlockPosX, playerBlockPosZ); + + + //with this code section we find the next chunk to check + if ((x == z) || ((x < 0) && (x == -z)) || ((x > 0) && (x == 1 - z))) + { + t = dx; + dx = -dz; + dz = t; + } + x += dx; + z += dz; + } + break; + } + return posToGenerate; + } + + /** + * Fills the posToRender with the position to render for the regionPos given in input + */ + public void getPosToRender(PosToRenderContainer posToRender, RegionPos regionPos, int playerPosX, + int playerPosZ) + { + LodRegion region = getRegion(regionPos.x, regionPos.z); + if (region != null) + region.getPosToRender(posToRender, playerPosX, playerPosZ, LodConfig.CLIENT.worldGenerator.generationPriority.get() == GenerationPriority.NEAR_FIRST); + } + + /** + * Determines how many vertical LODs could be used + * for the given region at the given detail level + */ + public int getMaxVerticalData(byte detailLevel, int posX, int posZ) + { + if (detailLevel > LodUtil.REGION_DETAIL_LEVEL) + throw new IllegalArgumentException("getMaxVerticalData given a level of [" + detailLevel + "] when [" + LodUtil.REGION_DETAIL_LEVEL + "] is the max."); + + LodRegion region = getRegion(detailLevel, posX, posZ); + if (region == null) + return 0; + + return region.getMaxVerticalData(detailLevel); + } + + /** + * Get the data point at the given X and Z coordinates + * in this dimension. + *
+ * Returns null if the LodChunk doesn't exist or + * is outside the loaded area. + */ + public long getData(byte detailLevel, int posX, int posZ, int verticalIndex) + { + if (detailLevel > LodUtil.REGION_DETAIL_LEVEL) + throw new IllegalArgumentException("getLodFromCoordinates given a level of \"" + detailLevel + "\" when \"" + LodUtil.REGION_DETAIL_LEVEL + "\" is the max."); + + LodRegion region = getRegion(detailLevel, posX, posZ); + if (region == null) + return DataPointUtil.EMPTY_DATA; + + return region.getData(detailLevel, posX, posZ, verticalIndex); + } + + + /** + * Get the data point at the given X and Z coordinates + * in this dimension. + *
+ * Returns null if the LodChunk doesn't exist or + * is outside the loaded area. + */ + public long getSingleData(byte detailLevel, int posX, int posZ) + { + if (detailLevel > LodUtil.REGION_DETAIL_LEVEL) + throw new IllegalArgumentException("getLodFromCoordinates given a level of \"" + detailLevel + "\" when \"" + LodUtil.REGION_DETAIL_LEVEL + "\" is the max."); + + LodRegion region = getRegion(detailLevel, posX, posZ); + if (region == null) + return DataPointUtil.EMPTY_DATA; + + return region.getSingleData(detailLevel, posX, posZ); + } + + /** Clears the given region */ + public void clear(byte detailLevel, int posX, int posZ) + { + if (detailLevel > LodUtil.REGION_DETAIL_LEVEL) + throw new IllegalArgumentException("getLodFromCoordinates given a level of \"" + detailLevel + "\" when \"" + LodUtil.REGION_DETAIL_LEVEL + "\" is the max."); + + LodRegion region = getRegion(detailLevel, posX, posZ); + if (region == null) + return; + + region.clear(detailLevel, posX, posZ); + } + + /** + * Returns if the buffer at the given array index needs + * to have its buffer regenerated. + */ + public boolean doesRegionNeedBufferRegen(int xIndex, int zIndex) + { + return regenRegionBuffer[xIndex][zIndex] || recreateRegionBuffer[xIndex][zIndex]; + } + + + /** + * Sets if the buffer at the given array index needs + * to have its buffer regenerated. + */ + public void setRegenRegionBufferByArrayIndex(int xIndex, int zIndex, boolean newRegen) + { + regenRegionBuffer[xIndex][zIndex] = newRegen; + } + + /** + * Get the data point at the given LevelPos + * in this dimension. + *
+ * Returns null if the LodChunk doesn't exist or + * is outside the loaded area. + */ + public void updateData(byte detailLevel, int posX, int posZ) + { + if (detailLevel > LodUtil.REGION_DETAIL_LEVEL) + throw new IllegalArgumentException("getLodFromCoordinates given a level of \"" + detailLevel + "\" when \"" + LodUtil.REGION_DETAIL_LEVEL + "\" is the max."); + + LodRegion region = getRegion(detailLevel, posX, posZ); + if (region == null) + return; + + region.updateArea(detailLevel, posX, posZ); + } + + /** Returns true if a region exists at the given LevelPos */ + public boolean doesDataExist(byte detailLevel, int posX, int posZ) + { + LodRegion region = getRegion(detailLevel, posX, posZ); + return region != null && region.doesDataExist(detailLevel, posX, posZ); + } + + /** + * Loads the region at the given RegionPos from file, + * if a file exists for that region. + */ + public LodRegion getRegionFromFile(RegionPos regionPos, byte detailLevel, + DistanceGenerationMode generationMode, VerticalQuality verticalQuality) + { + return fileHandler != null ? fileHandler.loadRegionFromFile(detailLevel, regionPos, generationMode, verticalQuality) : null; + } + + /** Save all dirty regions in this LodDimension to file. */ + public void saveDirtyRegionsToFileAsync() + { + fileHandler.saveDirtyRegionsToFileAsync(); + } + + + /** Return true if the chunk has been pregenerated in game */ + //public boolean isChunkPreGenerated(int xChunkPos, int zChunkPos) + //{ + // + // LodRegion region = getRegion(LodUtil.CHUNK_DETAIL_LEVEL, xChunkPos, zChunkPos); + // if (region == null) + // return false; + // + // return region.isChunkPreGenerated(xChunkPos, zChunkPos); + //} + + /** + * Returns whether the region at the given RegionPos + * is within the loaded range. + */ + public boolean regionIsInRange(int regionX, int regionZ) + { + int xIndex = (regionX - center.x) + halfWidth; + int zIndex = (regionZ - center.z) + halfWidth; + + return xIndex >= 0 && xIndex < width && zIndex >= 0 && zIndex < width; + } + + /** Returns the dimension's center region position X value */ + public int getCenterRegionPosX() + { + return center.x; + } + + /** Returns the dimension's center region position Z value */ + public int getCenterRegionPosZ() + { + return center.z; + } + + /** returns the width of the dimension in regions */ + public int getWidth() + { + // we want to get the length directly from the + // source to make sure it is in sync with region + // and isRegionDirty + return regions != null ? regions.length : width; + } + + /** Update the width of this dimension, in regions */ + public void setRegionWidth(int newWidth) + { + width = newWidth; + halfWidth = width/ 2; + + regions = new LodRegion[width][width]; + isRegionDirty = new boolean[width][width]; + regenRegionBuffer = new boolean[width][width]; + recreateRegionBuffer = new boolean[width][width]; + + // populate isRegionDirty + for (int i = 0; i < width; i++) + for (int j = 0; j < width; j++) + isRegionDirty[i][j] = false; + } + + + @Override + public String toString() + { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("Dimension : \n"); + for (LodRegion[] lodRegions : regions) + { + for (LodRegion region : lodRegions) + { + if (region == null) + stringBuilder.append("n"); + else + stringBuilder.append(region.getMinDetailLevel()); + stringBuilder.append("\t"); + } + stringBuilder.append("\n"); + } + return stringBuilder.toString(); + } + + public boolean GetIsRegionDirty(int i, int j) + { + return isRegionDirty[i][j]; + } + + public void SetIsRegionDirty(int i, int j, boolean val) + { + isRegionDirty[i][j] = val; + } +} diff --git a/src/main/java/com/seibel/lod/objects/LodRegion.java b/src/main/java/com/seibel/lod/objects/LodRegion.java new file mode 100644 index 000000000..f4719897e --- /dev/null +++ b/src/main/java/com/seibel/lod/objects/LodRegion.java @@ -0,0 +1,609 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.objects; + +import com.seibel.lod.enums.DistanceGenerationMode; +import com.seibel.lod.enums.VerticalQuality; +import com.seibel.lod.util.DataPointUtil; +import com.seibel.lod.util.DetailDistanceUtil; +import com.seibel.lod.util.LevelPosUtil; +import com.seibel.lod.util.LodUtil; + +/** + * This object holds all loaded LevelContainers acting as a quad tree + * for a given region.

+ * + * Coordinate Standard:
+ * Coordinate called posX or posZ are relative LevelPos coordinates
+ * unless stated otherwise.
+ * + * @author Leonardo Amato + * @version 10-10-2021 + */ +public class LodRegion +{ + /** Number of detail level supported by a region */ + private static final byte POSSIBLE_LOD = 10; + + + /** Holds the lowest (least detailed) detail level in this region */ + private byte minDetailLevel; + + /** + * This holds all data for this region + */ + private final LevelContainer[] dataContainer; + + /** This chunk Pos has been generated */ + //private final boolean[] preGeneratedChunkPos; + + /** the generation mode for this region */ + private final DistanceGenerationMode generationMode; + /** the vertical quality of this region */ + private final VerticalQuality verticalQuality; + + /** this region's x RegionPos */ + public final int regionPosX; + /** this region's z RegionPos */ + public final int regionPosZ; + + public LodRegion(byte minDetailLevel, RegionPos regionPos, DistanceGenerationMode generationMode, VerticalQuality verticalQuality) + { + this.minDetailLevel = minDetailLevel; + this.regionPosX = regionPos.x; + this.regionPosZ = regionPos.z; + this.verticalQuality = verticalQuality; + this.generationMode = generationMode; + dataContainer = new LevelContainer[POSSIBLE_LOD]; + + + // Initialize all the different matrices + for (byte lod = minDetailLevel; lod <= LodUtil.REGION_DETAIL_LEVEL; lod++) + { + dataContainer[lod] = new VerticalLevelContainer(lod); + } + + boolean fileFound = false; + + /* + preGeneratedChunkPos = new boolean[32 * 32]; + if (MinecraftWrapper.INSTANCE.hasSinglePlayerServer() && LodConfig.CLIENT.worldGenerator.useExperimentalPreGenLoading.get()) + { + File regionFileDirHead; + File regionFileDirParent; + // local world + + ServerWorld serverWorld = LodUtil.getServerWorldFromDimension(MinecraftWrapper.INSTANCE.getCurrentDimension()); + + // provider needs a separate variable to prevent + // the compiler from complaining + StringBuilder string = new StringBuilder(); + try + { + ServerChunkProvider provider = serverWorld.getChunkSource(); + + //System.out.println(provider.dataStorage.dataFolder); + regionFileDirHead = new File(provider.dataStorage.dataFolder.getCanonicalFile().getParentFile().toPath().toAbsolutePath().toString() + File.separatorChar + "region", "r." + regionPosZ + "." + regionPosX + ".mca"); + if (regionFileDirHead.exists()) + { + regionFileDirParent = regionFileDirHead.getParentFile(); + //string.append(regionFileDirParent.toString()); + string.append(regionFileDirHead); + RegionFile regionFile = new RegionFile(regionFileDirHead, regionFileDirParent, true); + for (int x = 0; x < 32; x++) + { + for (int z = 0; z < 32; z++) + { + preGeneratedChunkPos[x * 32 + z] = regionFile.doesChunkExist(new ChunkPos(regionPosX * 32 + x, regionPosZ * 32 + z)); + } + } + + string.append("region " + regionPosX + " " + regionPosZ + "\n"); + for (int x = 0; x < 32; x++) + { + for (int z = 0; z < 32; z++) + { + //regionFile.doesChunkExist() + string.append(preGeneratedChunkPos[x * 32 + z] + "\t"); + } + string.append("\n"); + } + regionFile.close(); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + System.out.println(string); + }*/ + + } + + + /** Return true if the chunk has been pregenerated in game */ + //public boolean isChunkPreGenerated(int xChunkPos, int zChunkPos) + //{ + // xChunkPos = LevelPosUtil.getRegionModule(LodUtil.CHUNK_DETAIL_LEVEL, xChunkPos); + // zChunkPos = LevelPosUtil.getRegionModule(LodUtil.CHUNK_DETAIL_LEVEL, zChunkPos); + // return preGeneratedChunkPos[xChunkPos * 32 + zChunkPos]; + //} + + /** + * Inserts the data point into the region. + *

+ * TODO this will always return true unless it has + * @return true if the data was added successfully + */ + public boolean addData(byte detailLevel, int posX, int posZ, int verticalIndex, long data) + { + posX = LevelPosUtil.getRegionModule(detailLevel, posX); + posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); + + // The dataContainer could have null entries if the + // detailLevel changes. + if (this.dataContainer[detailLevel] == null) + { + this.dataContainer[detailLevel] = new VerticalLevelContainer(detailLevel); + } + + this.dataContainer[detailLevel].addData(data, posX, posZ, verticalIndex); + + return true; + } + + /** + * Inserts the vertical data into the region. + *

+ * TODO this will always return true unless it has + * @return true if the data was added successfully + */ + public boolean addVerticalData(byte detailLevel, int posX, int posZ, long[] data) + { + //position is already relative + //posX = LevelPosUtil.getRegionModule(detailLevel, posX); + //posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); + + // The dataContainer could have null entries if the + // detailLevel changes. + if (this.dataContainer[detailLevel] == null) + this.dataContainer[detailLevel] = new VerticalLevelContainer(detailLevel); + + return this.dataContainer[detailLevel].addVerticalData(data, posX, posZ); + } + + /** + * Get the dataPoint at the given relative position. + * @return the data at the relative pos and detail level, + * 0 if the data doesn't exist. + */ + public long getData(byte detailLevel, int posX, int posZ, int verticalIndex) + { + return dataContainer[detailLevel].getData(posX, posZ, verticalIndex); + } + + /** + * Get the dataPoint at the given relative position. + * @return the data at the relative pos and detail level, + * 0 if the data doesn't exist. + */ + public long getSingleData(byte detailLevel, int posX, int posZ) + { + return dataContainer[detailLevel].getSingleData(posX, posZ); + } + + /** + * Clears the datapoint at the given relative position + */ + public void clear(byte detailLevel, int posX, int posZ) + { + dataContainer[detailLevel].clear(posX, posZ); + } + + /** + * This method will fill the posToGenerate array with all levelPos that + * are render-able. + *

+ * TODO why don't we return the posToGenerate, it would make this easier to understand + */ + public void getPosToGenerate(PosToGenerateContainer posToGenerate, + int playerBlockPosX, int playerBlockPosZ) + { + getPosToGenerate(posToGenerate, LodUtil.REGION_DETAIL_LEVEL, 0, 0, playerBlockPosX, playerBlockPosZ); + + } + + /** + * A recursive method that fills the posToGenerate array with all levelPos that + * need to be generated. + *

+ * TODO why don't we return the posToGenerate, it would make this easier to understand + */ + private void getPosToGenerate(PosToGenerateContainer posToGenerate, byte detailLevel, + int childOffsetPosX, int childOffsetPosZ, int playerPosX, int playerPosZ) + { + // equivalent to 2^(...) + int size = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel); + + // calculate what LevelPos are in range to generate + int maxDistance = LevelPosUtil.maxDistance(detailLevel, childOffsetPosX, childOffsetPosZ, playerPosX, playerPosZ, regionPosX, regionPosZ); + + // determine this child's levelPos + byte childDetailLevel = (byte) (detailLevel - 1); + int childPosX = childOffsetPosX * 2; + int childPosZ = childOffsetPosZ * 2; + + int childSize = 1 << (LodUtil.REGION_DETAIL_LEVEL - childDetailLevel); + + byte targetDetailLevel = DetailDistanceUtil.getLodGenDetail(DetailDistanceUtil.getGenerationDetailFromDistance(maxDistance)).detailLevel; + if (targetDetailLevel <= detailLevel) + { + if (targetDetailLevel == detailLevel) + { + if (!doesDataExist(detailLevel, childOffsetPosX, childOffsetPosZ)) + posToGenerate.addPosToGenerate(detailLevel, childOffsetPosX + regionPosX * size, childOffsetPosZ + regionPosZ * size); + } + else + { + // we want at max one request per chunk (since the world generator creates chunks). + // So for lod smaller than a chunk, only recurse down + // the top right child + + if (detailLevel > LodUtil.CHUNK_DETAIL_LEVEL) + { + int ungeneratedChildren = 0; + + // make sure all children are generated to this detailLevel + for (int x = 0; x <= 1; x++) + { + for (int z = 0; z <= 1; z++) + { + if (!doesDataExist(childDetailLevel, childPosX + x, childPosZ + z)) + { + ungeneratedChildren++; + posToGenerate.addPosToGenerate(childDetailLevel, childPosX + x + regionPosX * childSize, childPosZ + z + regionPosZ * childSize); + } + } + } + + // only if all the children are correctly generated + // should we go deeper + if (ungeneratedChildren == 0) + for (int x = 0; x <= 1; x++) + for (int z = 0; z <= 1; z++) + getPosToGenerate(posToGenerate, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, playerPosZ); + } + else + { + // The detail Level is smaller than a chunk. + // Only recurse down the top right child. + + if (DetailDistanceUtil.getLodGenDetail(childDetailLevel).detailLevel <= (childDetailLevel)) + { + if (!doesDataExist(childDetailLevel, childPosX, childPosZ)) + posToGenerate.addPosToGenerate(childDetailLevel, childPosX + regionPosX * childSize, childPosZ + regionPosZ * childSize); + else + getPosToGenerate(posToGenerate, childDetailLevel, childPosX, childPosZ, playerPosX, playerPosZ); + } + } + } + } + // we have gone beyond the target Detail level + // we can stop generating + + } + + + /** + * This method will fill the posToRender array with all levelPos that + * are render-able. + *

+ * TODO why don't we return the posToRender, it would make this easier to understand + */ + public void getPosToRender(PosToRenderContainer posToRender, + int playerPosX, int playerPosZ, boolean requireCorrectDetailLevel) + { + getPosToRender(posToRender, LodUtil.REGION_DETAIL_LEVEL, 0, 0, playerPosX, playerPosZ, requireCorrectDetailLevel); + } + + /** + * This method will fill the posToRender array with all levelPos that + * are render-able. + *

+ * TODO why don't we return the posToRender, it would make this easier to understand + * TODO this needs some more comments, James was only able to figure out part of it + */ + private void getPosToRender(PosToRenderContainer posToRender, + byte detailLevel, int posX, int posZ, + int playerPosX, int playerPosZ, boolean requireCorrectDetailLevel) + { + // equivalent to 2^(...) + int size = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel); + + byte desiredLevel; + int maxDistance; + int minDistance; + int childLevel; + + + // calculate the LevelPos that are in range + maxDistance = LevelPosUtil.maxDistance(detailLevel, posX, posZ, playerPosX, playerPosZ, regionPosX, regionPosZ); + desiredLevel = DetailDistanceUtil.getLodDrawDetail(DetailDistanceUtil.getDrawDetailFromDistance(maxDistance)); + minDistance = LevelPosUtil.minDistance(detailLevel, posX, posZ, playerPosX, playerPosZ, regionPosX, regionPosZ); + childLevel = DetailDistanceUtil.getLodDrawDetail(DetailDistanceUtil.getDrawDetailFromDistance(minDistance)); + + if (detailLevel == childLevel - 1) + { + posToRender.addPosToRender(detailLevel, + posX + regionPosX * size, + posZ + regionPosZ * size); + } + else + //if (desiredLevel > detailLevel) + //{ + // we have gone beyond the target Detail level + // we can stop generating + //} else + if (desiredLevel == detailLevel) + { + posToRender.addPosToRender(detailLevel, + posX + regionPosX * size, + posZ + regionPosZ * size); + } + else //case where (detailLevel > desiredLevel) + { + int childPosX = posX * 2; + int childPosZ = posZ * 2; + byte childDetailLevel = (byte) (detailLevel - 1); + int childrenCount = 0; + + for (int x = 0; x <= 1; x++) + { + for (int z = 0; z <= 1; z++) + { + if (doesDataExist(childDetailLevel, childPosX + x, childPosZ + z)) + { + if (!requireCorrectDetailLevel) + childrenCount++; + else + getPosToRender(posToRender, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, playerPosZ, requireCorrectDetailLevel); + } + } + } + + + if (!requireCorrectDetailLevel) + { + // If all the four children exist go deeper + if (childrenCount == 4) + { + for (int x = 0; x <= 1; x++) + for (int z = 0; z <= 1; z++) + getPosToRender(posToRender, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, playerPosZ, requireCorrectDetailLevel); + } + else + { + posToRender.addPosToRender(detailLevel, + posX + regionPosX * size, + posZ + regionPosZ * size); + } + } + } + } + + + /** + * Updates all children. + *

+ * TODO could this be renamed mergeArea? + */ + public void updateArea(byte detailLevel, int posX, int posZ) + { + int width; + int startX; + int startZ; + + // TODO what are each of these loops updating? + for (byte down = (byte) (minDetailLevel + 1); down <= detailLevel; down++) + { + startX = LevelPosUtil.convert(detailLevel, posX, down); + startZ = LevelPosUtil.convert(detailLevel, posZ, down); + width = 1 << (detailLevel - down); + + for (int x = 0; x < width; x++) + for (int z = 0; z < width; z++) + update(down, startX + x, startZ + z); + } + + + for (byte up = (byte) (detailLevel + 1); up <= LodUtil.REGION_DETAIL_LEVEL; up++) + { + update(up, + LevelPosUtil.convert(detailLevel, posX, up), + LevelPosUtil.convert(detailLevel, posZ, up)); + } + } + + /** + * Update the child at the given relative Pos + *

+ * TODO could this be renamed mergeChildData? + */ + private void update(byte detailLevel, int posX, int posZ) + { + dataContainer[detailLevel].updateData(dataContainer[detailLevel - 1], posX, posZ); + } + + + /** + * Returns if data exists at the given relative Pos. + */ + public boolean doesDataExist(byte detailLevel, int posX, int posZ) + { + if (detailLevel < minDetailLevel) + return false; + + posX = LevelPosUtil.getRegionModule(detailLevel, posX); + posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); + + if (dataContainer[detailLevel] == null) + return false; + + return dataContainer[detailLevel].doesItExist(posX, posZ); + } + + /** + * Gets the generation mode for the data point at the given relative pos. + */ + public byte getGenerationMode(byte detailLevel, int posX, int posZ) + { + if (dataContainer[detailLevel].doesItExist(posX, posZ)) + // We take the bottom information always + // TODO what does that mean? bottom of what? + return DataPointUtil.getGenerationMode(dataContainer[detailLevel].getSingleData(posX, posZ)); + else + return DistanceGenerationMode.NONE.complexity; + } + + /** + * Returns the lowest (least detailed) detail level in this region + * TODO is that right? + */ + public byte getMinDetailLevel() + { + return minDetailLevel; + } + + /** + * Returns the LevelContainer for the detailLevel + * @throws IllegalArgumentException if the detailLevel is less than minDetailLevel + */ + public LevelContainer getLevel(byte detailLevel) + { + if (detailLevel < minDetailLevel) + throw new IllegalArgumentException("getLevel asked for a detail level that does not exist: minimum: [" + minDetailLevel + "] level requested: [" + detailLevel + "]"); + + return dataContainer[detailLevel]; + } + + /** + * Add the levelContainer to this Region, updating the minDetailLevel + * if necessary. + * @throws IllegalArgumentException if the LevelContainer's detailLevel + * is 2 or more detail levels lower than the + * minDetailLevel of this region. + */ + public void addLevelContainer(LevelContainer levelContainer) + { + if (levelContainer.getDetailLevel() < minDetailLevel - 1) + { + throw new IllegalArgumentException( + "the LevelContainer's detailLevel was " + + "[" + levelContainer.getDetailLevel() + "] but this region " + + "only allows adding LevelContainers with a " + + "detail level of [" + (minDetailLevel - 1) + "]"); + } + + if (levelContainer.getDetailLevel() == minDetailLevel - 1) + minDetailLevel = levelContainer.getDetailLevel(); + + dataContainer[levelContainer.getDetailLevel()] = levelContainer; + } + + // TODO James thinks cutTree and growTree (which he renamed to match cutTree) + // should have more descriptive names, to make sure the "Tree" portion isn't + // confused with Minecraft trees (the plant). + + /** + * Removes any dataContainers that are higher than + * the given detailLevel + */ + public void cutTree(byte detailLevel) + { + if (detailLevel > minDetailLevel) + { + for (byte detailLevelIndex = 0; detailLevelIndex < detailLevel; detailLevelIndex++) + dataContainer[detailLevelIndex] = null; + + minDetailLevel = detailLevel; + } + } + + /** + * Make this region more detailed to the detailLevel given. + * TODO is that correct? + */ + public void growTree(byte detailLevel) + { + if (detailLevel < minDetailLevel) + { + for (byte detailLevelIndex = (byte) (minDetailLevel - 1); detailLevelIndex >= detailLevel; detailLevelIndex--) + { + if (dataContainer[detailLevelIndex + 1] == null) + dataContainer[detailLevelIndex + 1] = new VerticalLevelContainer((byte) (detailLevelIndex + 1)); + + dataContainer[detailLevelIndex] = dataContainer[detailLevelIndex + 1].expand(); + } + minDetailLevel = detailLevel; + } + } + + /** + * return RegionPos of this lod region + */ + public RegionPos getRegionPos() + { + return new RegionPos(regionPosX, regionPosZ); + } + + /** + * Returns how many LODs are in this region + */ + public int getNumberOfLods() + { + int count = 0; + for (LevelContainer container : dataContainer) + count += container.getMaxNumberOfLods(); + + return count; + } + + public VerticalQuality getVerticalQuality() + { + return verticalQuality; + } + + public DistanceGenerationMode getGenerationMode() + { + return generationMode; + } + + public int getMaxVerticalData(byte detailLevel) + { + return dataContainer[detailLevel].getMaxVerticalData(); + } + + + @Override + public String toString() + { + return getLevel(LodUtil.REGION_DETAIL_LEVEL).toString(); + } +} diff --git a/src/main/java/com/seibel/lod/objects/LodWorld.java b/src/main/java/com/seibel/lod/objects/LodWorld.java new file mode 100644 index 000000000..95d4ec998 --- /dev/null +++ b/src/main/java/com/seibel/lod/objects/LodWorld.java @@ -0,0 +1,172 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.objects; + +import java.util.Hashtable; +import java.util.Map; + +import com.seibel.lod.proxy.ClientProxy; + +import net.minecraft.world.DimensionType; + +/** + * This stores all LODs for a given world. + * + * @author James Seibel + * @author Leonardo Amato + * @version 9-27-2021 + */ +public class LodWorld +{ + /** name of this world */ + private String worldName; + + /** dimensions in this world */ + private Map lodDimensions; + + /** If true then the LOD world is setup and ready to use */ + private boolean isWorldLoaded = false; + + /** the name given to the world if it isn't loaded */ + public static final String NO_WORLD_LOADED = "No world loaded"; + + + + public LodWorld() + { + worldName = NO_WORLD_LOADED; + } + + + + /** + * Set up the LodWorld with the given newWorldName.
+ * This should be done whenever loading a new world.

+ *

+ * Note a System.gc() call may be in order after calling this
+ * since a lot of LOD data is now homeless.
+ * @param newWorldName name of the world + */ + public void selectWorld(String newWorldName) + { + if (newWorldName.isEmpty()) + { + deselectWorld(); + return; + } + + if (worldName.equals(newWorldName)) + // don't recreate everything if we + // didn't actually change worlds + return; + + worldName = newWorldName; + lodDimensions = new Hashtable<>(); + isWorldLoaded = true; + } + + /** + * Set the worldName to "No world loaded" + * and clear the lodDimensions Map.
+ * This should be done whenever unloaded a world.

+ *

+ * Note a System.gc() call may be in order after calling this
+ * since a lot of LOD data is now homeless.
+ */ + public void deselectWorld() + { + worldName = NO_WORLD_LOADED; + lodDimensions = null; + isWorldLoaded = false; + } + + + /** + * Adds newDimension to this world, if a LodDimension + * already exists for the given dimension it is replaced. + */ + public void addLodDimension(LodDimension newDimension) + { + if (lodDimensions == null) + return; + + lodDimensions.put(newDimension.dimension, newDimension); + } + + /** + * Returns null if no LodDimension exists for the given dimension + */ + public LodDimension getLodDimension(DimensionType dimension) + { + if (lodDimensions == null) + return null; + + return lodDimensions.get(dimension); + } + + /** + * Resizes the max width in regions that each LodDimension + * should use. + */ + public void resizeDimensionRegionWidth(int newRegionWidth) + { + if (lodDimensions == null) + return; + + saveAllDimensions(); + + for (DimensionType key : lodDimensions.keySet()) + lodDimensions.get(key).setRegionWidth(newRegionWidth); + } + + /** + * Requests all dimensions save any dirty regions they may have. + */ + public void saveAllDimensions() + { + if (lodDimensions == null) + return; + + // TODO we should only print this if lods were actually saved to file + // but that requires a LodDimension.hasDirtyRegions() method or something similar + ClientProxy.LOGGER.info("Saving LODs"); + + for (DimensionType key : lodDimensions.keySet()) + lodDimensions.get(key).saveDirtyRegionsToFileAsync(); + } + + + public boolean getIsWorldNotLoaded() + { + return !isWorldLoaded; + } + + public String getWorldName() + { + return worldName; + } + + @Override + public String toString() + { + return "World name: " + worldName; + } +} + diff --git a/src/main/java/com/seibel/lod/objects/NearFarFogSettings.java b/src/main/java/com/seibel/lod/objects/NearFarFogSettings.java new file mode 100644 index 000000000..3c25c59c6 --- /dev/null +++ b/src/main/java/com/seibel/lod/objects/NearFarFogSettings.java @@ -0,0 +1,64 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.objects; + +import com.seibel.lod.enums.FogDistance; +import com.seibel.lod.enums.FogQuality; + +/** + * This object is just a replacement for an array + * to make things easier to understand in the LodRenderer. + * + * @author James Seibel + * @version 7-03-2021 + */ +public class NearFarFogSettings +{ + public final NearOrFarSetting near = new NearOrFarSetting(FogDistance.NEAR); + public final NearOrFarSetting far = new NearOrFarSetting(FogDistance.FAR); + + /** + * If true that means Minecraft is + * rendering fog + */ + public boolean vanillaIsRenderingFog = true; + + public NearFarFogSettings() + { + + } + + + + /** + * This holds all relevant data to rendering fog at either + * near or far distances. + */ + public static class NearOrFarSetting + { + public FogQuality quality = FogQuality.FANCY; + public FogDistance distance; + + public NearOrFarSetting(FogDistance newFogDistance) + { + distance = newFogDistance; + } + } +} diff --git a/src/main/java/com/seibel/lod/objects/PosToGenerateContainer.java b/src/main/java/com/seibel/lod/objects/PosToGenerateContainer.java new file mode 100644 index 000000000..5efa58d14 --- /dev/null +++ b/src/main/java/com/seibel/lod/objects/PosToGenerateContainer.java @@ -0,0 +1,210 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.objects; + +import com.seibel.lod.util.LevelPosUtil; + +/** + * Holds the levelPos that need to be generated. + * TODO is that correct? + * + * @author Leonardo Amato + * @version 9-27-2021 + */ +public class PosToGenerateContainer +{ + private final int playerPosX; + private final int playerPosZ; + private final byte farMinDetail; + private int nearSize; + private int farSize; + + // TODO what is the format of these two arrays? [detailLevel][4-children]? + private final int[][] nearPosToGenerate; + private final int[][] farPosToGenerate; + + + + + public PosToGenerateContainer(byte farMinDetail, int maxDataToGenerate, int playerPosX, int playerPosZ) + { + this.playerPosX = playerPosX; + this.playerPosZ = playerPosZ; + this.farMinDetail = farMinDetail; + nearSize = 0; + farSize = 0; + nearPosToGenerate = new int[maxDataToGenerate][4]; + farPosToGenerate = new int[maxDataToGenerate][4]; + } + + + + // TODO what is going on in this method? + public void addPosToGenerate(byte detailLevel, int posX, int posZ) + { + int distance = LevelPosUtil.minDistance(detailLevel, posX, posZ, playerPosX, playerPosZ); + int index; + + if (detailLevel >= farMinDetail) + { + // We are introducing a position in the far array + + if (farSize < farPosToGenerate.length) + farSize++; + + index = farSize - 1; + while (index > 0 && LevelPosUtil.compareDistance(distance, farPosToGenerate[index - 1][3]) <= 0) + { + farPosToGenerate[index][0] = farPosToGenerate[index - 1][0]; + farPosToGenerate[index][1] = farPosToGenerate[index - 1][1]; + farPosToGenerate[index][2] = farPosToGenerate[index - 1][2]; + farPosToGenerate[index][3] = farPosToGenerate[index - 1][3]; + index--; + } + + + if (index != farSize - 1 || farSize != farPosToGenerate.length) + { + farPosToGenerate[index][0] = detailLevel + 1; + farPosToGenerate[index][1] = posX; + farPosToGenerate[index][2] = posZ; + farPosToGenerate[index][3] = distance; + } + } + else + { + //We are introducing a position in the near array + + if (nearSize < nearPosToGenerate.length) + nearSize++; + + index = nearSize - 1; + while (index > 0 && LevelPosUtil.compareDistance(distance, nearPosToGenerate[index - 1][3]) <= 0) + { + nearPosToGenerate[index][0] = nearPosToGenerate[index - 1][0]; + nearPosToGenerate[index][1] = nearPosToGenerate[index - 1][1]; + nearPosToGenerate[index][2] = nearPosToGenerate[index - 1][2]; + nearPosToGenerate[index][3] = nearPosToGenerate[index - 1][3]; + index--; + } + + + if (index != nearSize - 1 || nearSize != nearPosToGenerate.length) + { + nearPosToGenerate[index][0] = detailLevel + 1; + nearPosToGenerate[index][1] = posX; + nearPosToGenerate[index][2] = posZ; + nearPosToGenerate[index][3] = distance; + } + } + } + + + + public int getNumberOfPos() + { + return nearSize + farSize; + } + + public int getNumberOfNearPos() + { + return nearSize; + } + + public int getNumberOfFarPos() + { + return farSize; + } + + // TODO what does getNth mean? could the name be more descriptive or is it just a index? + public int getNthDetail(int n, boolean near) + { + if (near) + return nearPosToGenerate[n][0]; + else + return farPosToGenerate[n][0]; + } + + public int getNthPosX(int n, boolean near) + { + if (near) + return nearPosToGenerate[n][1]; + else + return farPosToGenerate[n][1]; + } + + public int getNthPosZ(int n, boolean near) + { + if (near) + return nearPosToGenerate[n][2]; + else + return farPosToGenerate[n][2]; + } + + public int getNthGeneration(int n, boolean near) + { + if (near) + return nearPosToGenerate[n][3]; + else + return farPosToGenerate[n][3]; + } + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(); + builder.append('\n'); + builder.append('\n'); + builder.append('\n'); + builder.append("near pos to generate"); + builder.append('\n'); + for (int[] ints : nearPosToGenerate) + { + if (ints[0] == 0) + break; + builder.append(ints[0] - 1); + builder.append(" "); + builder.append(ints[1]); + builder.append(" "); + builder.append(ints[2]); + builder.append(" "); + builder.append(ints[3]); + builder.append('\n'); + } + builder.append('\n'); + + builder.append("far pos to generate"); + builder.append('\n'); + for (int[] ints : farPosToGenerate) + { + if (ints[0] == 0) + break; + builder.append(ints[0] - 1); + builder.append(" "); + builder.append(ints[1]); + builder.append(" "); + builder.append(ints[2]); + builder.append(" "); + builder.append(ints[3]); + builder.append('\n'); + } + return builder.toString(); + } +} diff --git a/src/main/java/com/seibel/lod/objects/PosToRenderContainer.java b/src/main/java/com/seibel/lod/objects/PosToRenderContainer.java new file mode 100644 index 000000000..ce01fb601 --- /dev/null +++ b/src/main/java/com/seibel/lod/objects/PosToRenderContainer.java @@ -0,0 +1,147 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.objects; + +import java.util.Arrays; + +import com.seibel.lod.proxy.ClientProxy; +import com.seibel.lod.util.LevelPosUtil; +import com.seibel.lod.util.LodUtil; + +/** + * @author Leonardo Amato + * @version 9-18-2021 + */ +public class PosToRenderContainer +{ + public byte minDetail; + private int regionPosX; + private int regionPosZ; + private int numberOfPosToRender; + private int[] posToRender; + /*TODO this population matrix could be converted to boolean to improve memory use + * no since bools are stored as bytes anyway - cola*/ + private byte[][] population; + + public PosToRenderContainer(byte minDetail, int regionPosX, int regionPosZ) + { + this.minDetail = minDetail; + this.numberOfPosToRender = 0; + this.regionPosX = regionPosX; + this.regionPosZ = regionPosZ; + int size = 1 << (LodUtil.REGION_DETAIL_LEVEL - minDetail); + posToRender = new int[size * size * 3]; + population = new byte[size][size]; + } + + public void addPosToRender(byte detailLevel, int posX, int posZ) + { + // When rapidly changing dimensions the bufferBuilder can cause this, + // James isn't sure why, but this will prevent an exception at + // the very least (while stilling logging the problem). + if (numberOfPosToRender >= posToRender.length) + { + // This is might be due to dimensions having a different width + // when first loading in + ClientProxy.LOGGER.error("Unable to addPosToRender. numberOfPosToRender [" + numberOfPosToRender + "] detailLevel [" + detailLevel + "] Pos [" + posX + "," + posZ + "]"); + numberOfPosToRender++; // incrementing so we can see how many pos over the limit we would go + return; + } + + //if(numberOfPosToRender >= posToRender.length) + // posToRender = Arrays.copyOf(posToRender, posToRender.length*2); + posToRender[numberOfPosToRender * 3] = detailLevel; + posToRender[numberOfPosToRender * 3 + 1] = posX; + posToRender[numberOfPosToRender * 3 + 2] = posZ; + numberOfPosToRender++; + population[LevelPosUtil.getRegionModule(minDetail, LevelPosUtil.convert(detailLevel, posX, minDetail))] + [LevelPosUtil.getRegionModule(minDetail, LevelPosUtil.convert(detailLevel, posZ, minDetail))] = (byte) (detailLevel + 1); + } + + public boolean contains(byte detailLevel, int posX, int posZ) + { + if (LevelPosUtil.getRegion(detailLevel, posX) == regionPosX && LevelPosUtil.getRegion(detailLevel, posZ) == regionPosZ) + return (population[LevelPosUtil.getRegionModule(minDetail, LevelPosUtil.convert(detailLevel, posX, minDetail))] + [LevelPosUtil.getRegionModule(minDetail, LevelPosUtil.convert(detailLevel, posZ, minDetail))] == (detailLevel + 1)); + else + return false; + } + + public void clear(byte minDetail, int regionPosX, int regionPosZ) + { + this.numberOfPosToRender = 0; + this.regionPosX = regionPosX; + this.regionPosZ = regionPosZ; + if (this.minDetail == minDetail) + { + Arrays.fill(posToRender, 0); + for (byte[] bytes : population) + Arrays.fill(bytes, (byte) 0); + } + else + { + this.minDetail = minDetail; + int size = 1 << (LodUtil.REGION_DETAIL_LEVEL - minDetail); + posToRender = new int[size * size * 3]; + population = new byte[size][size]; + } + } + + public int getNumberOfPos() + { + return numberOfPosToRender; + } + + public byte getNthDetailLevel(int n) + { + return (byte) posToRender[n * 3]; + } + + public int getNthPosX(int n) + { + return posToRender[n * 3 + 1]; + } + + public int getNthPosZ(int n) + { + return posToRender[n * 3 + 2]; + } + + @Override + public String toString() + { + + StringBuilder builder = new StringBuilder(); + builder.append("To render "); + builder.append(numberOfPosToRender); + builder.append('\n'); + for (int i = 0; i < numberOfPosToRender; i++) + { + builder.append(posToRender[i * 3]); + builder.append(" "); + builder.append(posToRender[i * 3 + 1]); + builder.append(" "); + builder.append(posToRender[i * 3 + 2]); + builder.append('\n'); + } + builder.append('\n'); + return builder.toString(); + } +} diff --git a/src/main/java/com/seibel/lod/objects/RegionPos.java b/src/main/java/com/seibel/lod/objects/RegionPos.java new file mode 100644 index 000000000..22657c571 --- /dev/null +++ b/src/main/java/com/seibel/lod/objects/RegionPos.java @@ -0,0 +1,91 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.objects; + +import com.seibel.lod.util.LodUtil; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; + +/** + * This object is similar to ChunkPos or BlockPos. + * + * @author James Seibel + * @version 8-21-2021 + */ +public class RegionPos +{ + public int x; + public int z; + + + /** + * Default Constructor

+ *

+ * Sets x and z to 0 + */ + public RegionPos() + { + x = 0; + z = 0; + } + + /** simple constructor that sets x and z to new x and z. */ + public RegionPos(int newX, int newZ) + { + x = newX; + z = newZ; + } + + /** Converts from a BlockPos to a RegionPos */ + public RegionPos(BlockPos pos) + { + this(new ChunkPos(pos)); + } + + /** Converts from a ChunkPos to a RegionPos */ + public RegionPos(ChunkPos pos) + { + x = Math.floorDiv(pos.x, LodUtil.REGION_WIDTH_IN_CHUNKS); + z = Math.floorDiv(pos.z, LodUtil.REGION_WIDTH_IN_CHUNKS); + } + + /** Returns the ChunkPos at the center of this region */ + public ChunkPos chunkPos() + { + return new ChunkPos( + (x * LodUtil.REGION_WIDTH_IN_CHUNKS) + LodUtil.REGION_WIDTH_IN_CHUNKS / 2, + (z * LodUtil.REGION_WIDTH_IN_CHUNKS) + LodUtil.REGION_WIDTH_IN_CHUNKS / 2); + } + + /** Returns the BlockPos at the center of this region */ + public BlockPos blockPos() + { + return chunkPos().getWorldPosition() + .offset(LodUtil.CHUNK_WIDTH / 2, 0, LodUtil.CHUNK_WIDTH / 2); + } + + + @Override + public String toString() + { + return "(" + x + "," + z + ")"; + } +} diff --git a/src/main/java/com/seibel/lod/objects/VerticalLevelContainer.java b/src/main/java/com/seibel/lod/objects/VerticalLevelContainer.java new file mode 100644 index 000000000..f692baa1d --- /dev/null +++ b/src/main/java/com/seibel/lod/objects/VerticalLevelContainer.java @@ -0,0 +1,233 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.objects; + +import com.seibel.lod.util.DataPointUtil; +import com.seibel.lod.util.DetailDistanceUtil; +import com.seibel.lod.util.LevelPosUtil; +import com.seibel.lod.util.LodUtil; +import com.seibel.lod.util.ThreadMapUtil; + +/** + * + * @author Leonardo Amato + * @version ?? + */ +public class VerticalLevelContainer implements LevelContainer +{ + + public final byte detailLevel; + public final int size; + public final int maxVerticalData; + + public final long[] dataContainer; + + public VerticalLevelContainer(byte detailLevel) + { + this.detailLevel = detailLevel; + size = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel); + maxVerticalData = DetailDistanceUtil.getMaxVerticalData(detailLevel); + dataContainer = new long[size * size * DetailDistanceUtil.getMaxVerticalData(detailLevel)]; + } + + @Override + public byte getDetailLevel() + { + return detailLevel; + } + + @Override + public void clear(int posX, int posZ) + { + posX = LevelPosUtil.getRegionModule(detailLevel, posX); + posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); + for (int verticalIndex = 0; verticalIndex < maxVerticalData; verticalIndex++) + { + dataContainer[posX * size * maxVerticalData + posZ * maxVerticalData + verticalIndex] = DataPointUtil.EMPTY_DATA; + } + } + + @Override + public boolean addData(long data, int posX, int posZ, int verticalIndex) + { + posX = LevelPosUtil.getRegionModule(detailLevel, posX); + posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); + dataContainer[posX * size * maxVerticalData + posZ * maxVerticalData + verticalIndex] = data; + return true; + } + + @Override + public boolean addVerticalData(long[] data, int posX, int posZ) + { + posX = LevelPosUtil.getRegionModule(detailLevel, posX); + posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); + for (int verticalIndex = 0; verticalIndex < maxVerticalData; verticalIndex++) + dataContainer[posX * size * maxVerticalData + posZ * maxVerticalData + verticalIndex] = data[verticalIndex]; + return true; + } + + @Override + public boolean addSingleData(long data, int posX, int posZ) + { + return addData(data, posX, posZ, 0); + } + + @Override + public long getData(int posX, int posZ, int verticalIndex) + { + posX = LevelPosUtil.getRegionModule(detailLevel, posX); + posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); + return dataContainer[posX * size * maxVerticalData + posZ * maxVerticalData + verticalIndex]; + } + + @Override + public long getSingleData(int posX, int posZ) + { + return getData(posX, posZ, 0); + } + + @Override + public int getMaxVerticalData() + { + return maxVerticalData; + } + + public int getSize() + { + return size; + } + + @Override + public boolean doesItExist(int posX, int posZ) + { + posX = LevelPosUtil.getRegionModule(detailLevel, posX); + posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); + return DataPointUtil.doesItExist(getSingleData(posX, posZ)); + } + + public VerticalLevelContainer(byte[] inputData) + { + int tempIndex; + int index = 0; + long newData; + detailLevel = inputData[index]; + index++; + maxVerticalData = inputData[index]; + index++; + size = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel); + int x = size * size * maxVerticalData; + this.dataContainer = new long[x]; + for (int i = 0; i < x; i++) + { + newData = 0; + for (tempIndex = 0; tempIndex < 8; tempIndex++) + newData += (((long) inputData[index + tempIndex]) & 0xff) << (8 * tempIndex); + index += 8; + dataContainer[i] = newData; + } + } + + @Override + public LevelContainer expand() + { + return new VerticalLevelContainer((byte) (getDetailLevel() - 1)); + } + + @Override + public void updateData(LevelContainer lowerLevelContainer, int posX, int posZ) + { + //We reset the array + long[] dataToMerge = ThreadMapUtil.getVerticalUpdateArray(detailLevel); + + int lowerMaxVertical = dataToMerge.length / 4; + int childPosX; + int childPosZ; + long[] data; + posX = LevelPosUtil.getRegionModule(detailLevel, posX); + posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); + for (int x = 0; x <= 1; x++) + { + for (int z = 0; z <= 1; z++) + { + childPosX = 2 * posX + x; + childPosZ = 2 * posZ + z; + for (int verticalIndex = 0; verticalIndex < lowerMaxVertical; verticalIndex++) + dataToMerge[(z * 2 + x) * lowerMaxVertical + verticalIndex] = lowerLevelContainer.getData(childPosX, childPosZ, verticalIndex); + } + } + data = DataPointUtil.mergeMultiData(dataToMerge, lowerMaxVertical, getMaxVerticalData()); + + addVerticalData(data, posX, posZ); + } + + @Override + public byte[] toDataString() + { + int index = 0; + int x = size * size * maxVerticalData; + int tempIndex; + long current; + + byte[] tempData = ThreadMapUtil.getSaveContainer(detailLevel); + + tempData[index] = detailLevel; + index++; + tempData[index] = (byte) maxVerticalData; + index++; + + for (int i = 0; i < x; i++) + { + current = dataContainer[i]; + for (tempIndex = 0; tempIndex < 8; tempIndex++) + tempData[index + tempIndex] = (byte) (current >>> (8 * tempIndex)); + index += 8; + } + return tempData; + } + + @Override + @SuppressWarnings("unused") + public String toString() + { + /* + StringBuilder stringBuilder = new StringBuilder(); + int size = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel); + stringBuilder.append(detailLevel); + stringBuilder.append(DATA_DELIMITER); + for (int x = 0; x < size; x++) + { + for (int z = 0; z < size; z++) + { + //Converting the dataToHex + stringBuilder.append(Long.toHexString(dataContainer[x][z][0])); + stringBuilder.append(DATA_DELIMITER); + } + } + return stringBuilder.toString(); + */ + return " "; + } + + @Override + public int getMaxNumberOfLods() + { + return size * size * getMaxVerticalData(); + } +} diff --git a/src/main/java/com/seibel/lod/proxy/ClientProxy.java b/src/main/java/com/seibel/lod/proxy/ClientProxy.java new file mode 100644 index 000000000..e99e09f64 --- /dev/null +++ b/src/main/java/com/seibel/lod/proxy/ClientProxy.java @@ -0,0 +1,397 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.proxy; + +import com.seibel.lod.wrappers.Chunk.ChunkWrapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.glfw.GLFW; + +import com.mojang.blaze3d.matrix.MatrixStack; +import com.seibel.lod.builders.bufferBuilding.LodBufferBuilder; +import com.seibel.lod.builders.lodBuilding.LodBuilder; +import com.seibel.lod.builders.worldGeneration.LodGenWorker; +import com.seibel.lod.builders.worldGeneration.LodWorldGenerator; +import com.seibel.lod.config.LodConfig; +import com.seibel.lod.enums.DistanceGenerationMode; +import com.seibel.lod.objects.LodDimension; +import com.seibel.lod.objects.LodWorld; +import com.seibel.lod.objects.RegionPos; +import com.seibel.lod.render.LodRenderer; +import com.seibel.lod.util.DataPointUtil; +import com.seibel.lod.util.DetailDistanceUtil; +import com.seibel.lod.util.LodUtil; +import com.seibel.lod.util.ThreadMapUtil; +import com.seibel.lod.wrappers.MinecraftWrapper; + +import net.minecraft.profiler.IProfiler; +import net.minecraft.util.text.StringTextComponent; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.world.BlockEvent; +import net.minecraftforge.event.world.ChunkEvent; +import net.minecraftforge.event.world.WorldEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +/** + * This handles all events sent to the client, + * and is the starting point for most of the mod. + * @author James_Seibel + * @version 10-23-2021 + */ +public class ClientProxy +{ + public static final Logger LOGGER = LogManager.getLogger("LOD"); + + /** + * there is some setup that should only happen once, + * once this is true that setup has completed + */ + private boolean firstTimeSetupComplete = false; + + private static final LodWorld lodWorld = new LodWorld(); + private static final LodBuilder lodBuilder = new LodBuilder(); + private static final LodBufferBuilder lodBufferBuilder = new LodBufferBuilder(); + private static LodRenderer renderer = new LodRenderer(lodBufferBuilder); + private static final LodWorldGenerator lodWorldGenerator = LodWorldGenerator.INSTANCE; + + private boolean configOverrideReminderPrinted = false; + + private final MinecraftWrapper mc = MinecraftWrapper.INSTANCE; + + + /** This is used to determine if the LODs should be regenerated */ + public static int previousChunkRenderDistance = 0; + /** This is used to determine if the LODs should be regenerated */ + public static int previousLodRenderDistance = 0; + + /** + * can be set if we want to recalculate variables related + * to the LOD view distance + */ + private boolean recalculateWidths = false; + + public ClientProxy() + { + + } + + + //==============// + // render event // + //==============// + + /** Do any setup that is required to draw LODs and then tell the LodRenderer to draw. */ + public void renderLods(MatrixStack mcMatrixStack, float partialTicks) + { + // comment out when creating a release + // applyConfigOverrides(); + + // clear any out of date objects + mc.clearFrameObjectCache(); + + try + { + // only run the first time setup once + if (!firstTimeSetupComplete) + firstFrameSetup(); + + + if (mc.getPlayer() == null || lodWorld.getIsWorldNotLoaded()) + return; + + LodDimension lodDim = lodWorld.getLodDimension(mc.getCurrentDimension()); + if (lodDim == null) + return; + + DetailDistanceUtil.updateSettings(); + viewDistanceChangedEvent(); + playerMoveEvent(lodDim); + + lodDim.cutRegionNodesAsync((int) mc.getPlayer().getX(), (int) mc.getPlayer().getZ()); + lodDim.expandOrLoadRegionsAsync((int) mc.getPlayer().getX(), (int) mc.getPlayer().getZ()); + + + // Note to self: + // if "unspecified" shows up in the pie chart, it is + // possibly because the amount of time between sections + // is too small for the profiler to measure + IProfiler profiler = mc.getProfiler(); + profiler.pop(); // get out of "terrain" + profiler.push("LOD"); + + renderer.drawLODs(lodDim, mcMatrixStack, partialTicks, mc.getProfiler()); + + profiler.pop(); // end LOD + profiler.push("terrain"); // go back into "terrain" + + + // these can't be set until after the buffers are built (in renderer.drawLODs) + // otherwise the buffers may be set to the wrong size, or not changed at all + previousChunkRenderDistance = mc.getRenderDistance(); + previousLodRenderDistance = LodConfig.CLIENT.graphics.qualityOption.lodChunkRenderDistance.get(); + } + catch (Exception e) + { + LOGGER.error("client proxy: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** used in a development environment to change settings on the fly */ + private void applyConfigOverrides() + { + // remind the developer(s) that the config override is active + if (!configOverrideReminderPrinted) + { +// mc.getPlayer().sendMessage(new StringTextComponent("LOD experimental build 1.5.1"), mc.getPlayer().getUUID()); +// mc.getPlayer().sendMessage(new StringTextComponent("Here be dragons!"), mc.getPlayer().getUUID()); + + mc.getPlayer().sendMessage(new StringTextComponent("Debug settings enabled!"), mc.getPlayer().getUUID()); + configOverrideReminderPrinted = true; + } + +// LodConfig.CLIENT.graphics.drawResolution.set(HorizontalResolution.BLOCK); +// LodConfig.CLIENT.worldGenerator.generationResolution.set(HorizontalResolution.BLOCK); + // requires a world restart? +// LodConfig.CLIENT.worldGenerator.lodQualityMode.set(VerticalQuality.VOXEL); + +// LodConfig.CLIENT.graphics.fogQualityOption.fogDistance.set(FogDistance.FAR); +// LodConfig.CLIENT.graphics.fogQualityOption.fogDrawOverride.set(FogDrawOverride.FANCY); +// LodConfig.CLIENT.graphics.fogQualityOption.disableVanillaFog.set(true); +// LodConfig.CLIENT.graphics.shadingMode.set(ShadingMode.DARKEN_SIDES); + +// LodConfig.CLIENT.graphics.advancedGraphicsOption.vanillaOverdraw.set(VanillaOverdraw.DYNAMIC); + +// LodConfig.CLIENT.graphics.advancedGraphicsOption.gpuUploadMethod.set(GpuUploadMethod.BUFFER_STORAGE); + +// LodConfig.CLIENT.worldGenerator.distanceGenerationMode.set(DistanceGenerationMode.SURFACE); +// LodConfig.CLIENT.graphics.qualityOption.lodChunkRenderDistance.set(128); +// LodConfig.CLIENT.worldGenerator.lodDistanceCalculatorType.set(DistanceCalculatorType.LINEAR); +// LodConfig.CLIENT.worldGenerator.allowUnstableFeatureGeneration.set(false); + +// LodConfig.CLIENT.buffers.rebuildTimes.set(BufferRebuildTimes.FREQUENT); + + LodConfig.CLIENT.advancedModOptions.debugging.enableDebugKeybindings.set(true); +// LodConfig.CLIENT.debugging.debugMode.set(DebugMode.SHOW_DETAIL); + } + + + //==============// + // forge events // + //==============// + + @SubscribeEvent + public void serverTickEvent(TickEvent.ServerTickEvent event) + { + if (mc.getPlayer() == null || lodWorld.getIsWorldNotLoaded()) + return; + + LodDimension lodDim = lodWorld.getLodDimension(mc.getPlayer().level.dimensionType()); + if (lodDim == null) + return; + + lodWorldGenerator.queueGenerationRequests(lodDim, renderer, lodBuilder); + } + + @SubscribeEvent + public void chunkLoadEvent(ChunkEvent.Load event) + { + lodBuilder.generateLodNodeAsync(new ChunkWrapper(event.getChunk()), lodWorld, event.getWorld(), DistanceGenerationMode.SERVER); + } + + @SubscribeEvent + public void worldSaveEvent(WorldEvent.Save event) + { + lodWorld.saveAllDimensions(); + } + + /** This is also called when a new dimension loads */ + @SubscribeEvent + public void worldLoadEvent(WorldEvent.Load event) + { + DataPointUtil.worldHeight = event.getWorld().getHeight(); + //LodNodeGenWorker.restartExecutorService(); + //ThreadMapUtil.clearMaps(); + + // the player just loaded a new world/dimension + lodWorld.selectWorld(LodUtil.getWorldID(event.getWorld())); + + // make sure the correct LODs are being rendered + // (if this isn't done the previous world's LODs may be drawn) + renderer.regenerateLODsNextFrame(); + } + + @SubscribeEvent + public void worldUnloadEvent(WorldEvent.Unload event) + { + // the player just unloaded a world/dimension + ThreadMapUtil.clearMaps(); + + + if (mc.getConnection().getLevel() == null) + { + // the player just left the server + + // if this isn't done unfinished tasks may be left in the queue + // preventing new LodChunks form being generated + //LodNodeGenWorker.restartExecutorService(); + //ThreadMapUtil.clearMaps(); + + LodWorldGenerator.INSTANCE.numberOfChunksWaitingToGenerate.set(0); + lodWorld.deselectWorld(); + + + // hopefully this should reduce issues related to the buffer builder + // breaking when changing worlds. + renderer.destroyBuffers(); + recalculateWidths = true; + renderer = new LodRenderer(lodBufferBuilder); + + + // make sure the nilled objects are freed. + // (this prevents an out of memory error when + // changing worlds) + System.gc(); + } + } + + @SubscribeEvent + public void blockChangeEvent(BlockEvent event) + { + if (event.getClass() == BlockEvent.BreakEvent.class || + event.getClass() == BlockEvent.EntityPlaceEvent.class || + event.getClass() == BlockEvent.EntityMultiPlaceEvent.class || + event.getClass() == BlockEvent.FluidPlaceBlockEvent.class || + event.getClass() == BlockEvent.PortalSpawnEvent.class) + { + // recreate the LOD where the blocks were changed + lodBuilder.generateLodNodeAsync(new ChunkWrapper(event.getWorld().getChunk(event.getPos())), lodWorld, event.getWorld()); + } + } + + @SubscribeEvent + public void onKeyInput(InputEvent.KeyInputEvent event) + { + if (LodConfig.CLIENT.advancedModOptions.debugging.enableDebugKeybindings.get() + && event.getKey() == GLFW.GLFW_KEY_F4 && event.getAction() == GLFW.GLFW_PRESS) + { + LodConfig.CLIENT.advancedModOptions.debugging.debugMode.set(LodConfig.CLIENT.advancedModOptions.debugging.debugMode.get().getNext()); + } + + if (LodConfig.CLIENT.advancedModOptions.debugging.enableDebugKeybindings.get() + && event.getKey() == GLFW.GLFW_KEY_F6 && event.getAction() == GLFW.GLFW_PRESS) + { + LodConfig.CLIENT.advancedModOptions.debugging.drawLods.set(!LodConfig.CLIENT.advancedModOptions.debugging.drawLods.get()); + } + } + + + + + //============// + // LOD events // + //============// + + /** Re-centers the given LodDimension if it needs to be. */ + private void playerMoveEvent(LodDimension lodDim) + { + // make sure the dimension is centered + RegionPos playerRegionPos = new RegionPos(mc.getPlayer().blockPosition()); + RegionPos worldRegionOffset = new RegionPos(playerRegionPos.x - lodDim.getCenterRegionPosX(), playerRegionPos.z - lodDim.getCenterRegionPosZ()); + if (worldRegionOffset.x != 0 || worldRegionOffset.z != 0) + { + lodWorld.saveAllDimensions(); + lodDim.move(worldRegionOffset); + //LOGGER.info("offset: " + worldRegionOffset.x + "," + worldRegionOffset.z + "\t center: " + lodDim.getCenterX() + "," + lodDim.getCenterZ()); + } + } + + + /** Re-sizes all LodDimensions if they need to be. */ + private void viewDistanceChangedEvent() + { + // calculate how wide the dimension(s) should be in regions + int chunksWide; + if (mc.getClientWorld().dimensionType().hasCeiling()) + chunksWide = Math.min(LodConfig.CLIENT.graphics.qualityOption.lodChunkRenderDistance.get(), LodUtil.CEILED_DIMENSION_MAX_RENDER_DISTANCE) * 2 + 1; + else + chunksWide = LodConfig.CLIENT.graphics.qualityOption.lodChunkRenderDistance.get() * 2 + 1; + + int newWidth = (int) Math.ceil(chunksWide / (float) LodUtil.REGION_WIDTH_IN_CHUNKS); + // make sure we have an odd number of regions + newWidth += (newWidth & 1) == 0 ? 1 : 2; + + // do the dimensions need to change in size? + if (lodBuilder.defaultDimensionWidthInRegions != newWidth || recalculateWidths) + { + lodWorld.saveAllDimensions(); + + // update the dimensions to fit the new width + lodWorld.resizeDimensionRegionWidth(newWidth); + lodBuilder.defaultDimensionWidthInRegions = newWidth; + renderer.setupBuffers(lodWorld.getLodDimension(mc.getClientWorld().dimensionType())); + + recalculateWidths = false; + //LOGGER.info("new dimension width in regions: " + newWidth + "\t potential: " + newWidth ); + } + DetailDistanceUtil.updateSettings(); + } + + + /** This event is called once during the first frame Minecraft renders in the world. */ + public void firstFrameSetup() + { + // make sure the GlProxy is created before the LodBufferBuilder needs it + GlProxy.getInstance(); + + firstTimeSetupComplete = true; + } + + /** this method reset some static data every time we change world */ + private void resetMod() + { + ThreadMapUtil.clearMaps(); + LodGenWorker.restartExecutorService(); + + } + + + + + //================// + // public getters // + //================// + + public static LodWorld getLodWorld() + { + return lodWorld; + } + + public static LodBuilder getLodBuilder() + { + return lodBuilder; + } + + public static LodRenderer getRenderer() + { + return renderer; + } +} diff --git a/src/main/java/com/seibel/lod/proxy/GlProxy.java b/src/main/java/com/seibel/lod/proxy/GlProxy.java new file mode 100644 index 000000000..cfecd809c --- /dev/null +++ b/src/main/java/com/seibel/lod/proxy/GlProxy.java @@ -0,0 +1,244 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.proxy; + +import org.lwjgl.glfw.GLFW; +import org.lwjgl.opengl.GL; +import org.lwjgl.opengl.GLCapabilities; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.seibel.lod.ModInfo; +import com.seibel.lod.enums.GlProxyContext; +import com.seibel.lod.wrappers.MinecraftWrapper; + +/** + * A singleton that holds references to different openGL contexts + * and GPU capabilities. + * + *

+ * Helpful OpenGL resources:

+ *

+ * https://www.seas.upenn.edu/~pcozzi/OpenGLInsights/OpenGLInsights-AsynchronousBufferTransfers.pdf
+ * https://learnopengl.com/Advanced-OpenGL/Advanced-Data
+ * https://gamedev.stackexchange.com/questions/91995/edit-vbo-data-or-create-a-new-one

+ * + * @author James Seibel + * @version 10-23-2021 + */ +public class GlProxy +{ + private static GlProxy instance = null; + + private static final MinecraftWrapper mc = MinecraftWrapper.INSTANCE; + + /** Minecraft's GLFW window */ + public final long minecraftGlContext; + /** Minecraft's GL context */ + public final GLCapabilities minecraftGlCapabilities; + + /** the LodBuilder's GLFW window */ + public final long lodBuilderGlContext; + /** the LodBuilder's GL context */ + public final GLCapabilities lodBuilderGlCapabilities; + + /** + * This is just used for debugging, hopefully it can be removed once + * the context switching is more stable. + */ + public Thread lodBuilderOwnerThread = null; + + /** Does this computer's GPU support fancy fog? */ + public final boolean fancyFogAvailable; + + /** Requires OpenGL 4.5, and offers the best buffer uploading */ + public final boolean bufferStorageSupported; + + /** Requires OpenGL 3.0 */ + public final boolean mapBufferRangeSupported; + + + private GlProxy() + { + ClientProxy.LOGGER.error("Creating " + GlProxy.class.getSimpleName() + "... If this is the last message you see in the log there must have been a OpenGL error."); + + // getting Minecraft's context has to be done on the render thread, + // where the GL context is + if (!RenderSystem.isOnRenderThread()) + throw new IllegalStateException(GlProxy.class.getSimpleName() + " was created outside the render thread!"); + + + + //============================// + // create the builder context // + //============================// + + // get Minecraft's context + minecraftGlContext = GLFW.glfwGetCurrentContext(); + minecraftGlCapabilities = GL.getCapabilities(); + + // create the LodBuilder's context + + // Hopefully this shouldn't cause any issues with other mods that need custom contexts + // (although the number that do should be relatively few) + GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); + + // create an invisible window to hold the context + lodBuilderGlContext = GLFW.glfwCreateWindow(640, 480, "LOD window", 0L, minecraftGlContext); + GLFW.glfwMakeContextCurrent(lodBuilderGlContext); + lodBuilderGlCapabilities = GL.createCapabilities(); + + // Since this is called on the render thread, make sure the Minecraft context is used in the end + GLFW.glfwMakeContextCurrent(minecraftGlContext); + GL.setCapabilities(minecraftGlCapabilities); + + + + + //==============================// + // determine the OpenGL version // + //==============================// + + bufferStorageSupported = minecraftGlCapabilities.OpenGL45; + mapBufferRangeSupported = minecraftGlCapabilities.OpenGL30; + + if (!minecraftGlCapabilities.OpenGL15) + { + String errorMessage = ModInfo.READABLE_NAME + " was initializing " + GlProxy.class.getSimpleName() + " and discoverd this GPU doesn't support OpenGL 1.5 or greater."; + mc.crashMinecraft(errorMessage + " Sorry I couldn't tell you sooner :(", new UnsupportedOperationException("This GPU doesn't support OpenGL 1.5 or greater.")); + } + + if (!bufferStorageSupported) + { + String fallBackVersion = mapBufferRangeSupported ? "3.0" : "1.5"; + + ClientProxy.LOGGER.error("This GPU doesn't support OpenGL 4.5, falling back to OpenGL " + fallBackVersion + ". This may cause stuttering and reduced performance."); + } + + + + + + //==================================// + // get any GPU related capabilities // + //==================================// + + // see if this GPU can run fancy fog + fancyFogAvailable = GL.getCapabilities().GL_NV_fog_distance; + + if (!fancyFogAvailable) + ClientProxy.LOGGER.info("This GPU does not support GL_NV_fog_distance. This means that the fancy fog option will not be available."); + + + + + // GlProxy creation success + ClientProxy.LOGGER.error(GlProxy.class.getSimpleName() + " creation successful. OpenGL smiles upon you this day."); + } + + + + + /** + * A wrapper function to make switching contexts easier.
+ * Does nothing if the calling thread is already using newContext. + */ + public void setGlContext(GlProxyContext newContext) + { + GlProxyContext currentContext = getGlContext(); + + // we don't have to change the context, we're already there. + if (currentContext == newContext) + return; + + + long contextPointer = 0L; + GLCapabilities newGlCapabilities = null; + + // get the pointer(s) for this context + switch (newContext) + { + case LOD_BUILDER: + contextPointer = lodBuilderGlContext; + newGlCapabilities = lodBuilderGlCapabilities; + break; + + case MINECRAFT: + contextPointer = minecraftGlContext; + newGlCapabilities = minecraftGlCapabilities; + break; + + default: // default should never happen, it is just here to make the compiler happy + case NONE: + // 0L is equivalent to null + break; + } + + + GLFW.glfwMakeContextCurrent(contextPointer); + GL.setCapabilities(newGlCapabilities); + + + + // used for debugging + if (newContext == GlProxyContext.LOD_BUILDER) + lodBuilderOwnerThread = Thread.currentThread(); + else if (newContext == GlProxyContext.NONE && currentContext == GlProxyContext.LOD_BUILDER) + lodBuilderOwnerThread = null; + } + + + + + + /** Returns this thread's OpenGL context. */ + public GlProxyContext getGlContext() + { + long currentContext = GLFW.glfwGetCurrentContext(); + + + if (currentContext == lodBuilderGlContext) + return GlProxyContext.LOD_BUILDER; + else if (currentContext == minecraftGlContext) + return GlProxyContext.MINECRAFT; + else if (currentContext == 0L) + return GlProxyContext.NONE; + else + // hopefully this shouldn't happen, but + // at least now we will be notified if it does happen + throw new IllegalStateException(Thread.currentThread().getName() + " has a unknown OpenGl context: [" + currentContext + "]. Minecraft context [" + minecraftGlContext + "], LodBuilder context [" + lodBuilderGlContext + "], no context [0]."); + } + + + public static GlProxy getInstance() + { + if (instance == null) + instance = new GlProxy(); + + return instance; + } + + + + + + + + +} diff --git a/src/main/java/com/seibel/lod/render/LodRenderer.java b/src/main/java/com/seibel/lod/render/LodRenderer.java new file mode 100644 index 000000000..fd8608a23 --- /dev/null +++ b/src/main/java/com/seibel/lod/render/LodRenderer.java @@ -0,0 +1,959 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.render; + +import com.mojang.blaze3d.matrix.MatrixStack; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; +import com.seibel.lod.builders.bufferBuilding.LodBufferBuilder; +import com.seibel.lod.builders.bufferBuilding.LodBufferBuilder.VertexBuffersAndOffset; +import com.seibel.lod.config.LodConfig; +import com.seibel.lod.enums.*; +import com.seibel.lod.handlers.ReflectionHandler; +import com.seibel.lod.objects.LodDimension; +import com.seibel.lod.objects.NearFarFogSettings; +import com.seibel.lod.objects.RegionPos; +import com.seibel.lod.proxy.ClientProxy; +import com.seibel.lod.proxy.GlProxy; +import com.seibel.lod.util.DetailDistanceUtil; +import com.seibel.lod.util.LevelPosUtil; +import com.seibel.lod.util.LodUtil; +import com.seibel.lod.wrappers.MinecraftWrapper; +import net.minecraft.client.renderer.ActiveRenderInfo; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.vertex.VertexBuffer; +import net.minecraft.potion.Effects; +import net.minecraft.profiler.IProfiler; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.util.math.vector.Matrix4f; +import net.minecraft.util.math.vector.Vector3d; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL15C; +import org.lwjgl.opengl.NVFogDistance; + +import java.util.HashSet; + + +/** + * This is where all the magic happens.
+ * This is where LODs are draw to the world. + * @author James Seibel + * @version 10-25-2021 + */ +public class LodRenderer +{ + /** + * this is the light used when rendering the LODs, + * it should be something different from what is used by Minecraft + */ + private static final int LOD_GL_LIGHT_NUMBER = GL15.GL_LIGHT2; + + /** + * If true the LODs colors will be replaced with + * a checkerboard, this can be used for debugging. + */ + public DebugMode previousDebugMode = DebugMode.OFF; + + private final MinecraftWrapper mc; + private final GameRenderer gameRender; + private IProfiler profiler; + private int farPlaneBlockDistance; + + + /** This is used to generate the buildable buffers */ + private final LodBufferBuilder lodBufferBuilder; + + /** Each VertexBuffer represents 1 region */ + private VertexBuffer[][][] vbos; + /** + * the OpenGL IDs for the vbos of the same indices. + * These have to be separate because we can't override the + * buffers in the VBOs (and we don't want to) + */ + private int[][][] storageBufferIds; + + private ChunkPos vbosCenter = new ChunkPos(0, 0); + + + /** This is used to determine if the LODs should be regenerated */ + private int[] previousPos = new int[] { 0, 0, 0 }; + + // these variables are used to determine if the buffers should be rebuilt + private float prevSkyBrightness = 0; + private double prevBrightness = 0; + private int prevRenderDistance = 0; + private long prevPlayerPosTime = 0; + private long prevVanillaChunkTime = 0; + private long prevChunkTime = 0; + + + /** This is used to determine if the LODs should be regenerated */ + private FogDistance prevFogDistance = FogDistance.NEAR_AND_FAR; + + /** + * if this is true the LOD buffers should be regenerated, + * provided they aren't already being regenerated. + */ + private volatile boolean partialRegen = false; + private volatile boolean fullRegen = true; + + /** + * This HashSet contains every chunk that Vanilla Minecraft + * is going to render + */ + public boolean[][] vanillaRenderedChunks; + public boolean vanillaRenderedChunksChanged; + public boolean vanillaRenderedChunksEmptySkip = false; + public int vanillaBlockRenderedDistance; + + final boolean vivecraftDetected = ReflectionHandler.INSTANCE.detectVivecraft(); + + + + + public LodRenderer(LodBufferBuilder newLodNodeBufferBuilder) + { + mc = MinecraftWrapper.INSTANCE; + gameRender = mc.getGameRenderer(); + + lodBufferBuilder = newLodNodeBufferBuilder; + } + + + + + + + + /** + * Besides drawing the LODs this method also starts + * the async process of generating the Buffers that hold those LODs. + * @param lodDim The dimension to draw, if null doesn't replace the current dimension. + * @param mcMatrixStack This matrix stack should come straight from MC's renderChunkLayer (or future equivalent) method + * @param partialTicks how far into the current tick this method was called. + */ + @SuppressWarnings("deprecation") + public void drawLODs(LodDimension lodDim, MatrixStack mcMatrixStack, float partialTicks, IProfiler newProfiler) + { + //=================================// + // determine if LODs should render // + //=================================// + + if (lodDim == null) + { + // if there aren't any loaded LodChunks + // don't try drawing anything + return; + } + + if (mc.getPlayer().getActiveEffectsMap().get(Effects.BLINDNESS) != null) + { + // if the player is blind don't render LODs, + // and don't change minecraft's fog + // which blindness relies on. + return; + } + + + + + + //===============// + // initial setup // + //===============// + + profiler = newProfiler; + profiler.push("LOD setup"); + + + // TODO move the buffer regeneration logic into its own class (probably called in the client proxy instead) + // starting here... + determineIfLodsShouldRegenerate(lodDim, partialTicks); + + //=================// + // create the LODs // + //=================// + + // only regenerate the LODs if: + // 1. we want to regenerate LODs + // 2. we aren't already regenerating the LODs + // 3. we aren't waiting for the build and draw buffers to swap + // (this is to prevent thread conflicts) + if (LodConfig.CLIENT.advancedModOptions.debugging.drawLods.get()) + { + if (lodBufferBuilder.buildableBuffers == null) + lodBufferBuilder.setupBuffers(lodDim); + + if ((partialRegen || fullRegen) && !lodBufferBuilder.generatingBuffers && !lodBufferBuilder.newBuffersAvailable()) + { + // generate the LODs on a separate thread to prevent stuttering or freezing + lodBufferBuilder.generateLodBuffersAsync(this, lodDim, mc.getPlayer().blockPosition(), fullRegen); + + // the regen process has been started, + // it will be done when lodBufferBuilder.newBuffersAvailable() + // is true + fullRegen = false; + partialRegen = false; + } + } + else + lodBufferBuilder.destroyBuffers(); + + // TODO move the buffer regeneration logic into its own class (probably called in the client proxy instead) + // ...ending here + + if (lodBufferBuilder.newBuffersAvailable()) + { + swapBuffers(); + } + + + //===========================// + // GL settings for rendering // + //===========================// + + // set the required open GL settings + + if (LodConfig.CLIENT.advancedModOptions.debugging.debugMode.get() == DebugMode.SHOW_DETAIL_WIREFRAME) + GL15.glPolygonMode(GL15.GL_FRONT_AND_BACK, GL15.GL_LINE); + else + GL15.glPolygonMode(GL15.GL_FRONT_AND_BACK, GL15.GL_FILL); + + GL15.glDisable(GL15.GL_TEXTURE_2D); + GL15.glEnable(GL15.GL_CULL_FACE); + GL15.glEnable(GL15.GL_COLOR_MATERIAL); + GL15.glEnable(GL15.GL_DEPTH_TEST); + + // enable transparent rendering + GL15.glBlendFunc(GL15.GL_SRC_ALPHA, GL15.GL_ONE_MINUS_SRC_ALPHA); + GL15.glEnable(GL15.GL_BLEND); + + // disable the lights Minecraft uses + GL15.glDisable(GL15.GL_LIGHT0); + GL15.glDisable(GL15.GL_LIGHT1); + + // get the default projection matrix, so we can + // reset it after drawing the LODs + float[] mcProjMatrixRaw = new float[16]; + GL15.glGetFloatv(GL15.GL_PROJECTION_MATRIX, mcProjMatrixRaw); + Matrix4f mcProjectionMatrix = new Matrix4f(mcProjMatrixRaw); + // OpenGl outputs their matrices in col,row form instead of row,col + // (or maybe vice versa I have no idea :P) + mcProjectionMatrix.transpose(); + + Matrix4f modelViewMatrix = offsetTheModelViewMatrix(mcMatrixStack, partialTicks); + vanillaBlockRenderedDistance = mc.getRenderDistance() * LodUtil.CHUNK_WIDTH; + // required for setupFog and setupProjectionMatrix + if (mc.getClientWorld().dimensionType().hasCeiling()) + farPlaneBlockDistance = Math.min(LodConfig.CLIENT.graphics.qualityOption.lodChunkRenderDistance.get(), LodUtil.CEILED_DIMENSION_MAX_RENDER_DISTANCE) * LodUtil.CHUNK_WIDTH; + else + farPlaneBlockDistance = LodConfig.CLIENT.graphics.qualityOption.lodChunkRenderDistance.get() * LodUtil.CHUNK_WIDTH; + + setupProjectionMatrix(mcProjectionMatrix, vanillaBlockRenderedDistance, partialTicks); + + // commented out until we can add shaders to handle lighting + //setupLighting(lodDim, partialTicks); + + NearFarFogSettings fogSettings = determineFogSettings(); + + // determine the current fog settings, so they can be + // reset after drawing the LODs + float defaultFogStartDist = GL15.glGetFloat(GL15.GL_FOG_START); + float defaultFogEndDist = GL15.glGetFloat(GL15.GL_FOG_END); + int defaultFogMode = GL15.glGetInteger(GL15.GL_FOG_MODE); + int defaultFogDistance = GlProxy.getInstance().fancyFogAvailable ? GL15.glGetInteger(NVFogDistance.GL_FOG_DISTANCE_MODE_NV) : -1; + + //===========// + // rendering // + //===========// + + profiler.popPush("LOD draw"); + + if (vbos != null && LodConfig.CLIENT.advancedModOptions.debugging.drawLods.get()) + { + ActiveRenderInfo renderInfo = mc.getGameRenderer().getMainCamera(); + Vector3d cameraDir = new Vector3d(renderInfo.getLookVector()); + + boolean cullingDisabled = LodConfig.CLIENT.graphics.advancedGraphicsOption.disableDirectionalCulling.get(); + boolean renderBufferStorage = LodConfig.CLIENT.graphics.advancedGraphicsOption.gpuUploadMethod.get() == GpuUploadMethod.BUFFER_STORAGE && GlProxy.getInstance().bufferStorageSupported; + + // used to determine what type of fog to render + int halfWidth = vbos.length / 2; + int quarterWidth = vbos.length / 4; + + // where the center of the built buffers is (needed when culling regions) + RegionPos vboCenterRegionPos = new RegionPos(vbosCenter); + + + for (int x = 0; x < vbos.length; x++) + { + for (int z = 0; z < vbos.length; z++) + { + RegionPos vboPos = new RegionPos( + x + vboCenterRegionPos.x - (lodDim.getWidth() / 2), + z + vboCenterRegionPos.z - (lodDim.getWidth() / 2)); + + if (cullingDisabled || RenderUtil.isRegionInViewFrustum(renderInfo.getBlockPosition(), cameraDir, vboPos.blockPos())) + { + if ((x > halfWidth - quarterWidth && x < halfWidth + quarterWidth) + && (z > halfWidth - quarterWidth && z < halfWidth + quarterWidth)) + setupFog(fogSettings.near.distance, fogSettings.near.quality); + else + setupFog(fogSettings.far.distance, fogSettings.far.quality); + + + if (storageBufferIds != null && renderBufferStorage) + for (int i = 0; i < storageBufferIds[x][z].length; i++) + drawStorageBuffer(vbos[x][z][i], storageBufferIds[x][z][i], modelViewMatrix); + else + for (int i = 0; i < vbos[x][z].length; i++) + drawVertexBuffer(vbos[x][z][i], modelViewMatrix); + } + } + } + } + + + //=========// + // cleanup // + //=========// + + profiler.popPush("LOD cleanup"); + + GL15.glPolygonMode(GL15.GL_FRONT_AND_BACK, GL15.GL_FILL); + GL15.glEnable(GL15.GL_TEXTURE_2D); + GL15.glDisable(LOD_GL_LIGHT_NUMBER); + GL15.glDisable(GL15.GL_BLEND); + // re-enable the lights Minecraft uses + GL15.glEnable(GL15.GL_LIGHT0); + GL15.glEnable(GL15.GL_LIGHT1); + RenderSystem.disableLighting(); + + // reset the fog settings so the normal chunks + // will be drawn correctly + cleanupFog(fogSettings, defaultFogStartDist, defaultFogEndDist, defaultFogMode, defaultFogDistance); + + // reset the projection matrix so anything drawn after + // the LODs will use the correct projection matrix + gameRender.resetProjectionMatrix(mcProjectionMatrix); + + // clear the depth buffer so anything drawn is drawn + // over the LODs + GL15.glClear(GL15.GL_DEPTH_BUFFER_BIT); + + + // end of internal LOD profiling + profiler.pop(); + } + + + /** This is where the actual drawing happens. */ + private void drawStorageBuffer(VertexBuffer vbo, int bufferStorageId, Matrix4f modelViewMatrix) + { + if (vbo == null) + return; + + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, bufferStorageId); + // 0L is the starting pointer + LodUtil.LOD_VERTEX_FORMAT.setupBufferState(0L); + + vbo.draw(modelViewMatrix, GL15.GL_QUADS); + + GL15C.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + LodUtil.LOD_VERTEX_FORMAT.clearBufferState(); + } + + + /** This is where the actual drawing happens. */ + private void drawVertexBuffer(VertexBuffer vbo, Matrix4f modelViewMatrix) + { + if (vbo == null) + return; + + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo.id); + // 0L is the starting pointer + LodUtil.LOD_VERTEX_FORMAT.setupBufferState(0L); + + vbo.draw(modelViewMatrix, GL15.GL_QUADS); + + GL15C.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + LodUtil.LOD_VERTEX_FORMAT.clearBufferState(); + } + + + + + //=================// + // Setup Functions // + //=================// + + @SuppressWarnings("deprecation") + private void setupFog(FogDistance fogDistance, FogQuality fogQuality) + { + if (fogQuality == FogQuality.OFF) + { + GL15.glDisable(GL15.GL_FOG); + return; + } + + if (fogDistance == FogDistance.NEAR_AND_FAR) + { + throw new IllegalArgumentException("setupFog doesn't accept the NEAR_AND_FAR fog distance."); + } + + // determine the fog distance mode to use + int glFogDistanceMode; + if (fogQuality == FogQuality.FANCY) + { + // fancy fog (fragment distance based fog) + glFogDistanceMode = NVFogDistance.GL_EYE_RADIAL_NV; + } + else + { + // fast fog (frustum distance based fog) + glFogDistanceMode = NVFogDistance.GL_EYE_PLANE_ABSOLUTE_NV; + } + + // the multipliers are percentages + // of the regular view distance. + if (fogDistance == FogDistance.FAR) + { + // the reason that I wrote fogEnd then fogStart backwards + // is because we are using fog backwards to how + // it is normally used, with it hiding near objects + // instead of far objects. + + if (fogQuality == FogQuality.FANCY) + { + // for more realistic fog when using FAR + if (LodConfig.CLIENT.graphics.fogQualityOption.fogDistance.get() == FogDistance.NEAR_AND_FAR) + RenderSystem.fogStart(farPlaneBlockDistance * 1.6f * 0.9f); + else + RenderSystem.fogStart(Math.min(vanillaBlockRenderedDistance * 1.5f, farPlaneBlockDistance * 0.9f * 1.6f)); + RenderSystem.fogEnd(farPlaneBlockDistance * 1.6f); + } + else if (fogQuality == FogQuality.FAST) + { + // for the far fog of the normal chunks + // to start right where the LODs' end use: + // end = 0.8f, start = 1.5f + RenderSystem.fogStart(farPlaneBlockDistance * 0.75f); + RenderSystem.fogEnd(farPlaneBlockDistance * 1.0f); + } + } + else if (fogDistance == FogDistance.NEAR) + { + if (fogQuality == FogQuality.FANCY) + { + RenderSystem.fogEnd(vanillaBlockRenderedDistance * 1.41f); + RenderSystem.fogStart(vanillaBlockRenderedDistance * 1.6f); + } + else if (fogQuality == FogQuality.FAST) + { + RenderSystem.fogEnd(vanillaBlockRenderedDistance * 1.0f); + RenderSystem.fogStart(vanillaBlockRenderedDistance * 1.5f); + } + } + + GL15.glEnable(GL15.GL_FOG); + RenderSystem.enableFog(); + RenderSystem.setupNvFogDistance(); + RenderSystem.fogMode(GlStateManager.FogMode.LINEAR); + + if (GlProxy.getInstance().fancyFogAvailable) + GL15.glFogi(NVFogDistance.GL_FOG_DISTANCE_MODE_NV, glFogDistanceMode); + } + + /** + * Revert any changes that were made to the fog + * and sets up the fog for Minecraft. + */ + @SuppressWarnings("deprecation") + private void cleanupFog(NearFarFogSettings fogSettings, + float defaultFogStartDist, float defaultFogEndDist, + int defaultFogMode, int defaultFogDistance) + { + RenderSystem.fogStart(defaultFogStartDist); + RenderSystem.fogEnd(defaultFogEndDist); + RenderSystem.fogMode(defaultFogMode); + + // this setting is only valid if the GPU supports fancy fog + if (GlProxy.getInstance().fancyFogAvailable) + GL15.glFogi(NVFogDistance.GL_FOG_DISTANCE_MODE_NV, defaultFogDistance); + + // disable fog if Minecraft wasn't rendering fog + // or we want it disabled + if (!fogSettings.vanillaIsRenderingFog + || LodConfig.CLIENT.graphics.fogQualityOption.disableVanillaFog.get()) + { + // Make fog render a infinite distance away. + // This doesn't technically disable Minecraft's fog + // so performance will probably be the same regardless, unlike + // Optifine's no fog setting. + + // we can't disable minecraft's fog outright because by default + // minecraft will re-enable the fog after our code + + RenderSystem.fogStart(0.0F); + RenderSystem.fogEnd(Float.MAX_VALUE); + RenderSystem.fogDensity(0.0F); + } + } + + + /** + * Translate the camera relative to the LodDimension's center, + * this is done since all LOD buffers are created in world space + * instead of object space. + * (since AxisAlignedBoundingBoxes (LODs) use doubles and thus have a higher + * accuracy vs the model view matrix, which only uses floats) + */ + private Matrix4f offsetTheModelViewMatrix(MatrixStack mcMatrixStack, float partialTicks) + { + // duplicate the last matrix + mcMatrixStack.pushPose(); + + + // get all relevant camera info + ActiveRenderInfo renderInfo = mc.getGameRenderer().getMainCamera(); + Vector3d projectedView = renderInfo.getPosition(); + + // translate the camera relative to the regions' center + // (AxisAlignedBoundingBoxes (LODs) use doubles and thus have a higher + // accuracy vs the model view matrix, which only uses floats) + BlockPos bufferPos = vbosCenter.getWorldPosition(); + double xDiff = projectedView.x - bufferPos.getX(); + double zDiff = projectedView.z - bufferPos.getZ(); + mcMatrixStack.translate(-xDiff, -projectedView.y, -zDiff); + + + + // get the modified model view matrix + Matrix4f lodModelViewMatrix = mcMatrixStack.last().pose(); + // remove the lod ModelViewMatrix + mcMatrixStack.popPose(); + + return lodModelViewMatrix; + } + + /** + * create a new projection matrix and send it over to the GPU + * @param currentProjectionMatrix this is Minecraft's current projection matrix + * @param vanillaBlockRenderedDistance Minecraft's vanilla far plane distance + * @param partialTicks how many ticks into the frame we are + */ + private void setupProjectionMatrix(Matrix4f currentProjectionMatrix, float vanillaBlockRenderedDistance, float partialTicks) + { + Matrix4f lodPoj; + float nearClipPlane = LodConfig.CLIENT.graphics.advancedGraphicsOption.useExtendedNearClipPlane.get() ? vanillaBlockRenderedDistance / 5 : 1; + float farClipPlane = farPlaneBlockDistance * LodUtil.CHUNK_WIDTH >> 1; + + if (vivecraftDetected) + { + //use modify clip plane method to modify the current projection matrix's clip planes. + lodPoj = ReflectionHandler.INSTANCE.Matrix4fModifyClipPlanes( + currentProjectionMatrix, + nearClipPlane, + farClipPlane); + } + else + { + // create the new projection matrix + lodPoj = Matrix4f.perspective( + getFov(partialTicks, true), + (float) this.mc.getWindow().getScreenWidth() / (float) this.mc.getWindow().getScreenHeight(), + nearClipPlane, + farClipPlane); + + // get Minecraft's un-edited projection matrix + // (this is before it is zoomed, distorted, etc.) + Matrix4f defaultMcProj = mc.getGameRenderer().getProjectionMatrix(mc.getGameRenderer().getMainCamera(), partialTicks, true); + // true here means use "use fov setting" (probably) + + + // this logic strips away the defaultMcProj matrix, so we + // can get the distortionMatrix, which represents all + // transformations, zooming, distortions, etc. done + // to Minecraft's Projection matrix + Matrix4f defaultMcProjInv = defaultMcProj.copy(); + defaultMcProjInv.invert(); + + Matrix4f distortionMatrix = defaultMcProjInv.copy(); + distortionMatrix.multiply(currentProjectionMatrix); + + + // edit the lod projection to match Minecraft's + // (so the LODs line up with the real world) + lodPoj.multiply(distortionMatrix); + } + + // send the projection over to the GPU + gameRender.resetProjectionMatrix(lodPoj); + } + + + ///** setup the lighting to be used for the LODs */ + /*private void setupLighting(LodDimension lodDimension, float partialTicks) + { + // Determine if the player has night vision + boolean playerHasNightVision = false; + if (this.mc.getPlayer() != null) + { + Iterator iterator = this.mc.getPlayer().getActiveEffects().iterator(); + while (iterator.hasNext()) + { + EffectInstance instance = iterator.next(); + if (instance.getEffect() == Effects.NIGHT_VISION) + { + playerHasNightVision = true; + break; + } + } + } + + float sunBrightness = lodDimension.dimension.hasSkyLight() ? mc.getSkyDarken(partialTicks) : 0.2f; + sunBrightness = playerHasNightVision ? 1.0f : sunBrightness; + float gamma = (float) mc.getOptions().gamma - 0.0f; + float dayEffect = (sunBrightness - 0.2f) * 1.25f; + float lightStrength = (gamma * 0.34f - 0.01f) * (1.0f - dayEffect) + dayEffect - 0.20f; //gamma * 0.2980392157f + 0.1647058824f + float blueLightStrength = (gamma * 0.44f + 0.12f) * (1.0f - dayEffect) + dayEffect - 0.20f; //gamma * 0.4235294118f + 0.2784313725f + + float[] lightAmbient = {lightStrength, lightStrength, blueLightStrength, 1.0f}; + + + // can be used for debugging + // if (partialTicks < 0.005) + // ClientProxy.LOGGER.debug(lightStrength); + + ByteBuffer temp = ByteBuffer.allocateDirect(16); + temp.order(ByteOrder.nativeOrder()); + GL15.glLightfv(LOD_GL_LIGHT_NUMBER, GL15.GL_AMBIENT, (FloatBuffer) temp.asFloatBuffer().put(lightAmbient).flip()); + GL15.glEnable(LOD_GL_LIGHT_NUMBER); // Enable the above lighting + + RenderSystem.enableLighting(); + }*/ + + /** Create all buffers that will be used. */ + public void setupBuffers(LodDimension lodDim) + { + lodBufferBuilder.setupBuffers(lodDim); + } + + + //======================// + // Other Misc Functions // + //======================// + + /** + * If this is called then the next time "drawLODs" is called + * the LODs will be regenerated; the same as if the player moved. + */ + public void regenerateLODsNextFrame() + { + fullRegen = true; + } + + + /** + * Replace the current Vertex Buffers with the newly + * created buffers from the lodBufferBuilder.

+ *

+ * For some reason this has to be called after the frame has been rendered, + * otherwise visual stuttering/rubber banding may happen. I'm not sure why... + */ + private void swapBuffers() + { + // replace the drawable buffers with + // the newly created buffers from the lodBufferBuilder + VertexBuffersAndOffset result = lodBufferBuilder.getVertexBuffers(); + vbos = result.vbos; + storageBufferIds = result.storageBufferIds; + vbosCenter = result.drawableCenterChunkPos; + } + + /** Calls the BufferBuilder's destroyBuffers method. */ + public void destroyBuffers() + { + lodBufferBuilder.destroyBuffers(); + } + + + private double getFov(float partialTicks, boolean useFovSetting) + { + return mc.getGameRenderer().getFov(mc.getGameRenderer().getMainCamera(), partialTicks, useFovSetting); + } + + + /** Return what fog settings should be used when rendering. */ + private NearFarFogSettings determineFogSettings() + { + NearFarFogSettings fogSettings = new NearFarFogSettings(); + + + FogQuality quality = ReflectionHandler.INSTANCE.getFogQuality(); + FogDrawOverride override = LodConfig.CLIENT.graphics.fogQualityOption.fogDrawOverride.get(); + + + fogSettings.vanillaIsRenderingFog = quality != FogQuality.OFF; + + + // use any fog overrides the user may have set + switch (override) + { + case FANCY: + quality = FogQuality.FANCY; + break; + + case NO_FOG: + quality = FogQuality.OFF; + break; + + case FAST: + quality = FogQuality.FAST; + break; + + case OPTIFINE_SETTING: + // don't override anything + break; + } + + + // only use fancy fog if the user's GPU can deliver + if (!GlProxy.getInstance().fancyFogAvailable && quality == FogQuality.FANCY) + { + quality = FogQuality.FAST; + } + + + // how different distances are drawn depends on the quality set + switch (quality) + { + case FANCY: + fogSettings.near.quality = FogQuality.FANCY; + fogSettings.far.quality = FogQuality.FANCY; + + switch (LodConfig.CLIENT.graphics.fogQualityOption.fogDistance.get()) + { + case NEAR_AND_FAR: + fogSettings.near.distance = FogDistance.NEAR; + fogSettings.far.distance = FogDistance.FAR; + break; + + case NEAR: + fogSettings.near.distance = FogDistance.NEAR; + fogSettings.far.distance = FogDistance.NEAR; + break; + + case FAR: + fogSettings.near.distance = FogDistance.FAR; + fogSettings.far.distance = FogDistance.FAR; + break; + } + break; + + case FAST: + fogSettings.near.quality = FogQuality.FAST; + fogSettings.far.quality = FogQuality.FAST; + + // fast fog setting should only have one type of + // fog, since the LODs are separated into a near + // and far portion; and fast fog is rendered from the + // frustrum's perspective instead of the camera + switch (LodConfig.CLIENT.graphics.fogQualityOption.fogDistance.get()) + { + case NEAR_AND_FAR: + case NEAR: + fogSettings.near.distance = FogDistance.NEAR; + fogSettings.far.distance = FogDistance.NEAR; + break; + + case FAR: + fogSettings.near.distance = FogDistance.FAR; + fogSettings.far.distance = FogDistance.FAR; + break; + } + break; + + case OFF: + fogSettings.near.quality = FogQuality.OFF; + fogSettings.far.quality = FogQuality.OFF; + break; + } + return fogSettings; + } + + + /** Determines if the LODs should have a fullRegen or partialRegen */ + private void determineIfLodsShouldRegenerate(LodDimension lodDim, float partialTicks) + { + + + short chunkRenderDistance = (short) mc.getRenderDistance(); + int vanillaRenderedChunksWidth = chunkRenderDistance * 2 + 2; + + //=============// + // full regens // + //=============// + + // check if the view distance changed + if (ClientProxy.previousLodRenderDistance != LodConfig.CLIENT.graphics.qualityOption.lodChunkRenderDistance.get() + || chunkRenderDistance != prevRenderDistance + || prevFogDistance != LodConfig.CLIENT.graphics.fogQualityOption.fogDistance.get()) + { + + vanillaRenderedChunks = new boolean[vanillaRenderedChunksWidth][vanillaRenderedChunksWidth]; + DetailDistanceUtil.updateSettings(); + fullRegen = true; + previousPos = LevelPosUtil.createLevelPos((byte) 4, mc.getPlayer().xChunk, mc.getPlayer().zChunk); + prevFogDistance = LodConfig.CLIENT.graphics.fogQualityOption.fogDistance.get(); + prevRenderDistance = chunkRenderDistance; + } + + // did the user change the debug setting? + if (LodConfig.CLIENT.advancedModOptions.debugging.debugMode.get() != previousDebugMode) + { + previousDebugMode = LodConfig.CLIENT.advancedModOptions.debugging.debugMode.get(); + fullRegen = true; + } + + + long newTime = System.currentTimeMillis(); + + // check if the player has moved + if (newTime - prevPlayerPosTime > LodConfig.CLIENT.advancedModOptions.buffers.rebuildTimes.get().playerMoveTimeout) + { + if (LevelPosUtil.getDetailLevel(previousPos) == 0 + || mc.getPlayer().xChunk != LevelPosUtil.getPosX(previousPos) + || mc.getPlayer().zChunk != LevelPosUtil.getPosZ(previousPos)) + { + vanillaRenderedChunks = new boolean[vanillaRenderedChunksWidth][vanillaRenderedChunksWidth]; + fullRegen = true; + previousPos = LevelPosUtil.createLevelPos((byte) 4, mc.getPlayer().xChunk, mc.getPlayer().zChunk); + } + prevPlayerPosTime = newTime; + } + + + + // determine how far the lighting has to + // change in order to rebuild the buffers + + // the max brightness is 1 and the minimum is 0.2 + float skyBrightness = lodDim.dimension.hasSkyLight() ? mc.getSkyDarken(partialTicks) : 0.2f; + float minLightingDifference; + switch (LodConfig.CLIENT.advancedModOptions.buffers.rebuildTimes.get()) + { + case FREQUENT: + minLightingDifference = 0.025f; + break; + case NORMAL: + minLightingDifference = 0.05f; + break; + default: + case RARE: + minLightingDifference = 0.1f; + break; + } + + // check if the lighting changed + if (Math.abs(skyBrightness - prevSkyBrightness) > minLightingDifference + // make sure the lighting gets to the max/minimum value + // (just in case the minLightingDifference is too large to notice the change) + || (skyBrightness == 1.0f && prevSkyBrightness != 1.0f) // noon + || (skyBrightness == 0.2f && prevSkyBrightness != 0.2f) // midnight + || mc.getOptions().gamma != prevBrightness) + { + fullRegen = true; + prevBrightness = mc.getOptions().gamma; + prevSkyBrightness = skyBrightness; + } + + //================// + // partial regens // + //================// + + + // check if the vanilla rendered chunks changed + if (newTime - prevVanillaChunkTime > LodConfig.CLIENT.advancedModOptions.buffers.rebuildTimes.get().renderedChunkTimeout) + { + if (vanillaRenderedChunksChanged) + { + partialRegen = true; + vanillaRenderedChunksChanged = false; + } + prevVanillaChunkTime = newTime; + } + + + // check if there is any newly generated terrain to show + if (newTime - prevChunkTime > LodConfig.CLIENT.advancedModOptions.buffers.rebuildTimes.get().chunkChangeTimeout) + { + if (lodDim.regenDimensionBuffers) + { + partialRegen = true; + lodDim.regenDimensionBuffers = false; + } + prevChunkTime = newTime; + } + + + + //==============// + // LOD skipping // + //==============// + + // determine which LODs should not be rendered close to the player + HashSet chunkPosToSkip = LodUtil.getNearbyLodChunkPosToSkip(lodDim, mc.getPlayer().blockPosition()); + int xIndex; + int zIndex; + for (ChunkPos pos : chunkPosToSkip) + { + vanillaRenderedChunksEmptySkip = false; + + xIndex = (pos.x - mc.getPlayer().xChunk) + (chunkRenderDistance + 1); + zIndex = (pos.z - mc.getPlayer().zChunk) + (chunkRenderDistance + 1); + + // sometimes we are given chunks that are outside the render distance, + // This prevents index out of bounds exceptions + if (xIndex >= 0 && zIndex >= 0 + && xIndex < vanillaRenderedChunks.length + && zIndex < vanillaRenderedChunks.length) + { + if (!vanillaRenderedChunks[xIndex][zIndex]) + { + vanillaRenderedChunks[xIndex][zIndex] = true; + vanillaRenderedChunksChanged = true; + lodDim.markRegionBufferToRegen(pos.getRegionX(), pos.getRegionZ()); + } + } + } + + + // if the player is high enough, draw all LODs + if (chunkPosToSkip.isEmpty() && mc.getPlayer().position().y > 256 && !vanillaRenderedChunksEmptySkip) + { + vanillaRenderedChunks = new boolean[vanillaRenderedChunksWidth][vanillaRenderedChunksWidth]; + vanillaRenderedChunksChanged = true; + vanillaRenderedChunksEmptySkip = true; + } + } + +} diff --git a/src/main/java/com/seibel/lod/render/RenderUtil.java b/src/main/java/com/seibel/lod/render/RenderUtil.java new file mode 100644 index 000000000..8835f4cda --- /dev/null +++ b/src/main/java/com/seibel/lod/render/RenderUtil.java @@ -0,0 +1,123 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.render; + +import com.seibel.lod.util.LodUtil; +import com.seibel.lod.wrappers.MinecraftWrapper; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.util.math.vector.Vector3d; + +/** + * This holds miscellaneous helper code + * to be used in the rendering process. + * + * @author James Seibel + * @version 10-19-2021 + */ +public class RenderUtil +{ + private static final MinecraftWrapper mc = MinecraftWrapper.INSTANCE; + + + /** + * Returns if the given ChunkPos is in the loaded area of the world. + * @param center the center of the loaded world (probably the player's ChunkPos) + */ + public static boolean isChunkPosInLoadedArea(ChunkPos pos, ChunkPos center) + { + return (pos.x >= center.x - mc.getRenderDistance() + && pos.x <= center.x + mc.getRenderDistance()) + && + (pos.z >= center.z - mc.getRenderDistance() + && pos.z <= center.z + mc.getRenderDistance()); + } + + /** + * Returns if the given coordinate is in the loaded area of the world. + * @param centerCoordinate the center of the loaded world + */ + public static boolean isCoordinateInLoadedArea(int x, int z, int centerCoordinate) + { + return (x >= centerCoordinate - mc.getRenderDistance() + && x <= centerCoordinate + mc.getRenderDistance()) + && + (z >= centerCoordinate - mc.getRenderDistance() + && z <= centerCoordinate + mc.getRenderDistance()); + } + + + /** + * Find the coordinates that are in the center half of the given + * 2D matrix, starting at (0,0) and going to (2 * lodRadius, 2 * lodRadius). + */ + public static boolean isCoordinateInNearFogArea(int i, int j, int lodRadius) + { + int halfRadius = lodRadius / 2; + + return (i >= lodRadius - halfRadius + && i <= lodRadius + halfRadius) + && + (j >= lodRadius - halfRadius + && j <= lodRadius + halfRadius); + } + + + /** + * Returns true if one of the region's 4 corners is in front + * of the camera. + */ + public static boolean isRegionInViewFrustum(BlockPos playerBlockPos, Vector3d cameraDir, BlockPos vboCenterPos) + { + // convert the vbo position into a direction vector + // starting from the player's position + Vector3d vboVec = new Vector3d(vboCenterPos.getX(), 0, vboCenterPos.getZ()); + Vector3d playerVec = new Vector3d(playerBlockPos.getX(), playerBlockPos.getY(), playerBlockPos.getZ()); + Vector3d vboCenterVec = vboVec.subtract(playerVec); + + + int halfRegionWidth = LodUtil.REGION_WIDTH / 2; + + // calculate the 4 corners + Vector3d vboSeVec = new Vector3d(vboCenterVec.x + halfRegionWidth, vboCenterVec.y, vboCenterVec.z + halfRegionWidth); + Vector3d vboSwVec = new Vector3d(vboCenterVec.x - halfRegionWidth, vboCenterVec.y, vboCenterVec.z + halfRegionWidth); + Vector3d vboNwVec = new Vector3d(vboCenterVec.x - halfRegionWidth, vboCenterVec.y, vboCenterVec.z - halfRegionWidth); + Vector3d vboNeVec = new Vector3d(vboCenterVec.x + halfRegionWidth, vboCenterVec.y, vboCenterVec.z - halfRegionWidth); + + // if any corner is visible, this region should be rendered + return isNormalizedVectorInViewFrustum(vboSeVec, cameraDir) || + isNormalizedVectorInViewFrustum(vboSwVec, cameraDir) || + isNormalizedVectorInViewFrustum(vboNwVec, cameraDir) || + isNormalizedVectorInViewFrustum(vboNeVec, cameraDir); + } + + /** + * Currently takes the dot product of the two vectors, + * but in the future could do more complicated frustum culling tests. + */ + private static boolean isNormalizedVectorInViewFrustum(Vector3d objectVector, Vector3d cameraDir) + { + // the -0.1 is to offer a slight buffer, so we are + // more likely to render LODs and thus, hopefully prevent + // flickering or odd disappearances + return objectVector.dot(cameraDir) > -0.1; + } +} diff --git a/src/main/java/com/seibel/lod/util/ColorUtil.java b/src/main/java/com/seibel/lod/util/ColorUtil.java new file mode 100644 index 000000000..d8456b9aa --- /dev/null +++ b/src/main/java/com/seibel/lod/util/ColorUtil.java @@ -0,0 +1,115 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.util; + +import java.awt.Color; + +import com.seibel.lod.wrappers.MinecraftWrapper; + +/** + * + * @author Cola + * @author Leonardo Amato + * @version ?? + */ +public class ColorUtil +{ + public static int rgbToInt(int red, int green, int blue) + { + return (0xFF << 24) | (red << 16) | (green << 8) | blue; + } + + public static int rgbToInt(int alpha, int red, int green, int blue) + { + return (alpha << 24) | (red << 16) | (green << 8) | blue; + } + + public static int getAlpha(int color) + { + return (color >> 24) & 0xFF; + } + + public static int getRed(int color) + { + return (color >> 16) & 0xFF; + } + + public static int getGreen(int color) + { + return (color >> 8) & 0xFF; + } + + public static int getBlue(int color) + { + return color & 0xFF; + } + + public static int applyShade(int color, int shade) + { + if (shade < 0) + return (getAlpha(color) << 24) | (Math.max(getRed(color) + shade, 0) << 16) | (Math.max(getGreen(color) + shade, 0) << 8) | Math.max(getBlue(color) + shade, 0); + else + return (getAlpha(color) << 24) | (Math.min(getRed(color) + shade, 255) << 16) | (Math.min(getGreen(color) + shade, 255) << 8) | Math.min(getBlue(color) + shade, 255); + } + + public static int applyShade(int color, float shade) + { + if (shade < 1) + return (getAlpha(color) << 24) | ((int) Math.max(getRed(color) * shade, 0) << 16) | ((int) Math.max(getGreen(color) * shade, 0) << 8) | (int) Math.max(getBlue(color) * shade, 0); + else + return (getAlpha(color) << 24) | ((int) Math.min(getRed(color) * shade, 255) << 16) | ((int) Math.min(getGreen(color) * shade, 255) << 8) | (int) Math.min(getBlue(color) * shade, 255); + } + + /** This method apply the lightmap to the color to use */ + public static int applyLightValue(int color, int skyLight, int blockLight) + { + int lightColor = MinecraftWrapper.INSTANCE.getColorIntFromLightMap(blockLight, skyLight); + int red = ColorUtil.getBlue(lightColor); + int green = ColorUtil.getGreen(lightColor); + int blue = ColorUtil.getRed(lightColor); + + return ColorUtil.multiplyRGBcolors(color, ColorUtil.rgbToInt(red, green, blue)); + } + + /** Edit the given color as an HSV (Hue Saturation Value) color */ + public static int applySaturationAndBrightnessMultipliers(int color, float saturationMultiplier, float brightnessMultiplier) + { + float[] hsv = Color.RGBtoHSB(getRed(color), getGreen(color), getBlue(color), null); + return Color.getHSBColor( + hsv[0], // hue + LodUtil.clamp(0.0f, hsv[1] * saturationMultiplier, 1.0f), + LodUtil.clamp(0.0f, hsv[2] * brightnessMultiplier, 1.0f)).getRGB(); + } + + /** Multiply 2 RGB colors */ + public static int multiplyRGBcolors(int color1, int color2) + { + return ((getAlpha(color1) * getAlpha(color2) / 255) << 24) | ((getRed(color1) * getRed(color2) / 255) << 16) | ((getGreen(color1) * getGreen(color2) / 255) << 8) | (getBlue(color1) * getBlue(color2) / 255); + } + + @SuppressWarnings("unused") + public static String toString(int color) + { + return Integer.toHexString(getAlpha(color)) + " " + + Integer.toHexString(getRed(color)) + " " + + Integer.toHexString(getGreen(color)) + " " + + Integer.toHexString(getBlue(color)); + } +} diff --git a/src/main/java/com/seibel/lod/util/DataPointUtil.java b/src/main/java/com/seibel/lod/util/DataPointUtil.java new file mode 100644 index 000000000..5bc568e90 --- /dev/null +++ b/src/main/java/com/seibel/lod/util/DataPointUtil.java @@ -0,0 +1,520 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.util; + +import static com.seibel.lod.builders.bufferBuilding.LodBufferBuilder.skyLightPlayer; + +import com.seibel.lod.enums.DistanceGenerationMode; + +/** + * + * @author Leonardo Amato + * @version ?? + */ +public class DataPointUtil +{ + /* + |a |a |a |a |r |r |r |r | + + |r |r |r |r |g |g |g |g | + + |g |g |g |g |b |b |b |b | + + |b |b |b |b |h |h |h |h | + + |h |h |h |h |h |h |d |d | + + |d |d |d |d |d |d |d |d | + + |bl |bl |bl |bl |sl |sl |sl |sl | + + |l |l |f |g |g |g |v |e | + + + */ + + // Reminder: bytes have range of [-128, 127]. + // When converting to or from an int a 128 should be added or removed. + // If there is a bug with color then it's probably caused by this. + + //To be used in the future for negative value + //public final static int MIN_DEPTH = -64; + //public final static int MIN_HEIGHT = -64; + public final static int EMPTY_DATA = 0; + public static int worldHeight = 256; + + public final static int ALPHA_DOWNSIZE_SHIFT = 4; + + //public final static int BLUE_COLOR_SHIFT = 0; + //public final static int GREEN_COLOR_SHIFT = 8; + //public final static int RED_COLOR_SHIFT = 16; + //public final static int ALPHA_COLOR_SHIFT = 24; + + public final static int BLUE_SHIFT = 36; + public final static int GREEN_SHIFT = BLUE_SHIFT + 8; + public final static int RED_SHIFT = BLUE_SHIFT + 16; + public final static int ALPHA_SHIFT = BLUE_SHIFT + 24; + + public final static int COLOR_SHIFT = 36; + + public final static int HEIGHT_SHIFT = 26; + public final static int DEPTH_SHIFT = 16; + public final static int BLOCK_LIGHT_SHIFT = 12; + public final static int SKY_LIGHT_SHIFT = 8; + //public final static int LIGHTS_SHIFT = SKY_LIGHT_SHIFT; + //public final static int VERTICAL_INDEX_SHIFT = 6; + public final static int FLAG_SHIFT = 5; + public final static int GEN_TYPE_SHIFT = 2; + public final static int VOID_SHIFT = 1; + public final static int EXISTENCE_SHIFT = 0; + + public final static long ALPHA_MASK = 0b1111; + public final static long RED_MASK = 0b1111_1111; + public final static long GREEN_MASK = 0b1111_1111; + public final static long BLUE_MASK = 0b1111_1111; + public final static long COLOR_MASK = 0b11111111_11111111_11111111; + public final static long HEIGHT_MASK = 0b11_1111_1111; + public final static long DEPTH_MASK = 0b11_1111_1111; + //public final static long LIGHTS_MASK = 0b1111_1111; + public final static long BLOCK_LIGHT_MASK = 0b1111; + public final static long SKY_LIGHT_MASK = 0b1111; + //public final static long VERTICAL_INDEX_MASK = 0b11; + public final static long FLAG_MASK = 0b1; + public final static long GEN_TYPE_MASK = 0b111; + public final static long VOID_MASK = 1; + public final static long EXISTENCE_MASK = 1; + + + public static long createVoidDataPoint(int generationMode) + { + long dataPoint = 0; + dataPoint += (generationMode & GEN_TYPE_MASK) << GEN_TYPE_SHIFT; + dataPoint += VOID_MASK << VOID_SHIFT; + dataPoint += EXISTENCE_MASK << EXISTENCE_SHIFT; + return dataPoint; + } + + public static long createDataPoint(int height, int depth, int color, int lightSky, int lightBlock, int generationMode, boolean flag) + { + return createDataPoint( + ColorUtil.getAlpha(color), + ColorUtil.getRed(color), + ColorUtil.getGreen(color), + ColorUtil.getBlue(color), + height, depth, lightSky, lightBlock, generationMode, flag); + } + + public static long createDataPoint(int alpha, int red, int green, int blue, int height, int depth, int lightSky, int lightBlock, int generationMode, boolean flag) + { + long dataPoint = 0; + dataPoint += (long) (alpha >>> ALPHA_DOWNSIZE_SHIFT) << ALPHA_SHIFT; + dataPoint += (red & RED_MASK) << RED_SHIFT; + dataPoint += (green & GREEN_MASK) << GREEN_SHIFT; + dataPoint += (blue & BLUE_MASK) << BLUE_SHIFT; + dataPoint += (height & HEIGHT_MASK) << HEIGHT_SHIFT; + dataPoint += (depth & DEPTH_MASK) << DEPTH_SHIFT; + dataPoint += (lightBlock & BLOCK_LIGHT_MASK) << BLOCK_LIGHT_SHIFT; + dataPoint += (lightSky & SKY_LIGHT_MASK) << SKY_LIGHT_SHIFT; + dataPoint += (generationMode & GEN_TYPE_MASK) << GEN_TYPE_SHIFT; + if (flag) dataPoint += FLAG_MASK << FLAG_SHIFT; + dataPoint += EXISTENCE_MASK << EXISTENCE_SHIFT; + + return dataPoint; + } + + public static short getHeight(long dataPoint) + { + return (short) ((dataPoint >>> HEIGHT_SHIFT) & HEIGHT_MASK); + } + + public static short getDepth(long dataPoint) + { + return (short) ((dataPoint >>> DEPTH_SHIFT) & DEPTH_MASK); + } + + public static short getAlpha(long dataPoint) + { + return (short) ((((dataPoint >>> ALPHA_SHIFT) & ALPHA_MASK) << ALPHA_DOWNSIZE_SHIFT) | 0b1111); + } + + public static short getRed(long dataPoint) + { + return (short) ((dataPoint >>> RED_SHIFT) & RED_MASK); + } + + public static short getGreen(long dataPoint) + { + return (short) ((dataPoint >>> GREEN_SHIFT) & GREEN_MASK); + } + + public static short getBlue(long dataPoint) + { + return (short) ((dataPoint >>> BLUE_SHIFT) & BLUE_MASK); + } + + public static byte getLightSky(long dataPoint) + { + return (byte) ((dataPoint >>> SKY_LIGHT_SHIFT) & SKY_LIGHT_MASK); + } + + public static byte getLightSkyAlt(long dataPoint) + { + if (skyLightPlayer == 0 && ((dataPoint >>> FLAG_SHIFT) & FLAG_MASK) == 1) + return 0; + else + return (byte) ((dataPoint >>> SKY_LIGHT_SHIFT) & SKY_LIGHT_MASK); + } + + public static byte getLightBlock(long dataPoint) + { + return (byte) ((dataPoint >>> BLOCK_LIGHT_SHIFT) & BLOCK_LIGHT_MASK); + } + + public static boolean getFlag(long dataPoint) + { + return ((dataPoint >>> FLAG_SHIFT) & FLAG_MASK) == 1; + } + + public static byte getGenerationMode(long dataPoint) + { + return (byte) ((dataPoint >>> GEN_TYPE_SHIFT) & GEN_TYPE_MASK); + } + + + public static boolean isVoid(long dataPoint) + { + return (((dataPoint >>> VOID_SHIFT) & VOID_MASK) == 1); + } + + public static boolean doesItExist(long dataPoint) + { + return (((dataPoint >>> EXISTENCE_SHIFT) & EXISTENCE_MASK) == 1); + } + + public static int getColor(long dataPoint) + { + return (int) (((dataPoint >>> COLOR_SHIFT) & COLOR_MASK) | (/*((dataPoint >>> (ALPHA_SHIFT - ALPHA_DOWNSIZE_SHIFT)) | 0b1111)*/255 << 24)); + } + + /** This is used to convert a dataPoint to string (useful for the print function) */ + @SuppressWarnings("unused") + public static String toString(long dataPoint) + { + return getHeight(dataPoint) + " " + + getDepth(dataPoint) + " " + + getAlpha(dataPoint) + " " + + getRed(dataPoint) + " " + + getBlue(dataPoint) + " " + + getGreen(dataPoint) + " " + + getLightBlock(dataPoint) + " " + + getLightSky(dataPoint) + " " + + getGenerationMode(dataPoint) + " " + + isVoid(dataPoint) + " " + + doesItExist(dataPoint) + '\n'; + } + + public static void shrinkArray(short[] array, int packetSize, int start, int length, int arraySize) + { + start *= packetSize; + length *= packetSize; + arraySize *= packetSize; + for (int i = 0; i < arraySize - start; i++) + { + array[start + i] = array[start + length + i]; + //remove comment to not leave garbage at the end + //array[start + packetSize + i] = 0; + } + } + + public static void extendArray(short[] array, int packetSize, int start, int length, int arraySize) + { + start *= packetSize; + length *= packetSize; + arraySize *= packetSize; + for (int i = arraySize - start - 1; i >= 0; i--) + { + array[start + length + i] = array[start + i]; + array[start + i] = 0; + } + } + + /** + * This method merge column of multiple data together + * @param dataToMerge one or more columns of data + * @param inputVerticalData vertical size of an input data + * @param maxVerticalData max vertical size of the merged data + * @return one column of correctly parsed data + */ + public static long[] mergeMultiData(long[] dataToMerge, int inputVerticalData, int maxVerticalData) + { + int size = dataToMerge.length / inputVerticalData; + + // We initialize the arrays that are going to be used + short[] heightAndDepth = ThreadMapUtil.getHeightAndDepth((worldHeight / 2 + 1) * 2); + long[] dataPoint = ThreadMapUtil.getVerticalDataArray(DetailDistanceUtil.getMaxVerticalData(0)); + + + int genMode = DistanceGenerationMode.SERVER.complexity; + boolean allEmpty = true; + boolean allVoid = true; + boolean allDefault; + long singleData; + + + short depth; + short height; + int count = 0; + int i; + int ii; + int dataIndex; + //We collect the indexes of the data, ordered by the depth + for (int index = 0; index < size; index++) + { + for (dataIndex = 0; dataIndex < inputVerticalData; dataIndex++) + { + singleData = dataToMerge[index * inputVerticalData + dataIndex]; + if (doesItExist(singleData)) + { + genMode = Math.min(genMode, getGenerationMode(singleData)); + allEmpty = false; + if (!isVoid(singleData)) + { + allVoid = false; + depth = getDepth(singleData); + height = getHeight(singleData); + + int botPos = -1; + int topPos = -1; + //values fall in between and possibly require extension of array + boolean botExtend = false; + boolean topExtend = false; + for (i = 0; i < count; i++) + { + if (depth <= heightAndDepth[i * 2] && depth >= heightAndDepth[i * 2 + 1]) + { + botPos = i; + break; + } + else if (depth < heightAndDepth[i * 2 + 1] && ((i + 1 < count && depth > heightAndDepth[(i + 1) * 2]) || i + 1 == count)) + { + botPos = i; + botExtend = true; + break; + } + } + for (i = 0; i < count; i++) + { + if (height <= heightAndDepth[i * 2] && height >= heightAndDepth[i * 2 + 1]) + { + topPos = i; + break; + } + else if (height < heightAndDepth[i * 2 + 1] && ((i + 1 < count && height > heightAndDepth[(i + 1) * 2]) || i + 1 == count)) + { + topPos = i; + topExtend = true; + break; + } + } + if (topPos == -1) + { + if (botPos == -1) + { + //whole block falls above + extendArray(heightAndDepth, 2, 0, 1, count); + heightAndDepth[0] = height; + heightAndDepth[1] = depth; + count++; + } + else if (!botExtend) + { + //only top falls above extending it there, while bottom is inside existing + shrinkArray(heightAndDepth, 2, 0, botPos, count); + heightAndDepth[0] = height; + count -= botPos; + } + else + { + //top falls between some blocks, extending those as well + shrinkArray(heightAndDepth, 2, 0, botPos, count); + heightAndDepth[0] = height; + heightAndDepth[1] = depth; + count -= botPos; + } + } + else if (!topExtend) + { + if (!botExtend) + //both top and bottom are within some exiting blocks, possibly merging them + heightAndDepth[topPos * 2 + 1] = heightAndDepth[botPos * 2 + 1]; + else + //top falls between some blocks, extending it there + heightAndDepth[topPos * 2 + 1] = depth; + shrinkArray(heightAndDepth, 2, topPos + 1, botPos - topPos, count); + count -= botPos - topPos; + } + else + { + if (!botExtend) + { + //only top is within some exiting block, extending it + topPos++; //to make it easier + heightAndDepth[topPos * 2] = height; + heightAndDepth[topPos * 2 + 1] = heightAndDepth[botPos * 2 + 1]; + shrinkArray(heightAndDepth, 2, topPos + 1, botPos - topPos, count); + count -= botPos - topPos; + } + else + { + //both top and bottom are outside existing blocks + shrinkArray(heightAndDepth, 2, topPos + 1, botPos - topPos, count); + count -= botPos - topPos; + extendArray(heightAndDepth, 2, topPos + 1, 1, count); + count++; + heightAndDepth[topPos * 2 + 2] = height; + heightAndDepth[topPos * 2 + 3] = depth; + } + } + } + } + else + break; + } + } + + //We check if there is any data that's not empty or void + if (allEmpty) + return dataPoint; + if (allVoid) + { + dataPoint[0] = createVoidDataPoint(genMode); + return dataPoint; + } + + //we limit the vertical portion to maxVerticalData + int j = 0; + while (count > maxVerticalData) + { + ii = worldHeight; + for (i = 0; i < count - 1; i++) + { + if (heightAndDepth[i * 2 + 1] - heightAndDepth[(i + 1) * 2] <= ii) + { + ii = heightAndDepth[i * 2 + 1] - heightAndDepth[(i + 1) * 2]; + j = i; + } + } + heightAndDepth[j * 2 + 1] = heightAndDepth[(j + 1) * 2 + 1]; + for (i = j + 1; i < count - 1; i++) + { + heightAndDepth[i * 2] = heightAndDepth[(i + 1) * 2]; + heightAndDepth[i * 2 + 1] = heightAndDepth[(i + 1) * 2 + 1]; + } + //System.arraycopy(heightAndDepth, j + 1, heightAndDepth, j, count - j - 1); + count--; + } + //As standard the vertical lods are ordered from top to bottom + for (j = count - 1; j >= 0; j--) + { + height = heightAndDepth[j * 2]; + depth = heightAndDepth[j * 2 + 1]; + + if ((depth == 0 && height == 0) || j >= heightAndDepth.length / 2) + break; + + int numberOfChildren = 0; + int tempAlpha = 0; + int tempRed = 0; + int tempGreen = 0; + int tempBlue = 0; + int tempLightBlock = 0; + int tempLightSky = 0; + byte tempGenMode = DistanceGenerationMode.SERVER.complexity; + allEmpty = true; + allVoid = true; + allDefault = true; + long data = 0; + + for (int index = 0; index < size; index++) + { + for (dataIndex = 0; dataIndex < inputVerticalData; dataIndex++) + { + singleData = dataToMerge[index * inputVerticalData + dataIndex]; + if (doesItExist(singleData) && !isVoid(singleData)) + { + + if ((depth <= getDepth(singleData) && getDepth(singleData) <= height) + || (depth <= getHeight(singleData) && getHeight(singleData) <= height)) + { + if (getHeight(singleData) > getHeight(data)) + data = singleData; + } + } + else + break; + } + if (!doesItExist(data)) + { + singleData = dataToMerge[index * inputVerticalData]; + data = createVoidDataPoint(getGenerationMode(singleData)); + } + + if (doesItExist(data)) + { + allEmpty = false; + if (!isVoid(data)) + { + numberOfChildren++; + allVoid = false; + tempAlpha += getAlpha(data); + tempRed += getRed(data); + tempGreen += getGreen(data); + tempBlue += getBlue(data); + tempLightBlock += getLightBlock(data); + tempLightSky += getLightSky(data); + if (!getFlag(data)) allDefault = false; + } + tempGenMode = (byte) Math.min(tempGenMode, getGenerationMode(data)); + } + else + tempGenMode = (byte) Math.min(tempGenMode, DistanceGenerationMode.NONE.complexity); + } + + if (allEmpty) + //no child has been initialized + dataPoint[j] = EMPTY_DATA; + else if (allVoid) + //all the children are void + dataPoint[j] = createVoidDataPoint(tempGenMode); + else + { + //we have at least 1 child + tempAlpha = tempAlpha / numberOfChildren; + tempRed = tempRed / numberOfChildren; + tempGreen = tempGreen / numberOfChildren; + tempBlue = tempBlue / numberOfChildren; + tempLightBlock = tempLightBlock / numberOfChildren; + tempLightSky = tempLightSky / numberOfChildren; + dataPoint[j] = createDataPoint(tempAlpha, tempRed, tempGreen, tempBlue, height, depth, tempLightSky, tempLightBlock, tempGenMode, allDefault); + } + } + return dataPoint; + } +} \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/util/DetailDistanceUtil.java b/src/main/java/com/seibel/lod/util/DetailDistanceUtil.java new file mode 100644 index 000000000..61f38942e --- /dev/null +++ b/src/main/java/com/seibel/lod/util/DetailDistanceUtil.java @@ -0,0 +1,171 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.util; + +import com.seibel.lod.config.LodConfig; +import com.seibel.lod.enums.DistanceGenerationMode; +import com.seibel.lod.enums.HorizontalQuality; +import com.seibel.lod.enums.HorizontalResolution; +import com.seibel.lod.wrappers.MinecraftWrapper; + +/** + * + * @author Leonardo Amato + * @version ?? + */ +public class DetailDistanceUtil +{ + private static final double genMultiplier = 1.0; + private static final double treeGenMultiplier = 1.0; + private static final double treeCutMultiplier = 1.0; + private static byte minGenDetail = LodConfig.CLIENT.graphics.qualityOption.drawResolution.get().detailLevel; + private static byte minDrawDetail = (byte) Math.max(LodConfig.CLIENT.graphics.qualityOption.drawResolution.get().detailLevel, LodConfig.CLIENT.graphics.qualityOption.drawResolution.get().detailLevel); + private static final int maxDetail = LodUtil.REGION_DETAIL_LEVEL + 1; + private static final int minDistance = 0; + private static int minDetailDistance = (int) (MinecraftWrapper.INSTANCE.getRenderDistance()*16 * 1.42f); + private static int maxDistance = LodConfig.CLIENT.graphics.qualityOption.lodChunkRenderDistance.get() * 16 * 2; + + + private static final HorizontalResolution[] lodGenDetails = { + HorizontalResolution.BLOCK, + HorizontalResolution.TWO_BLOCKS, + HorizontalResolution.FOUR_BLOCKS, + HorizontalResolution.HALF_CHUNK, + HorizontalResolution.CHUNK, + HorizontalResolution.CHUNK, + HorizontalResolution.CHUNK, + HorizontalResolution.CHUNK, + HorizontalResolution.CHUNK, + HorizontalResolution.CHUNK, + HorizontalResolution.CHUNK }; + + + + public static void updateSettings() + { + minDetailDistance = (int) (MinecraftWrapper.INSTANCE.getRenderDistance()*16 * 1.42f); + minGenDetail = LodConfig.CLIENT.graphics.qualityOption.drawResolution.get().detailLevel; + minDrawDetail = (byte) Math.max(LodConfig.CLIENT.graphics.qualityOption.drawResolution.get().detailLevel, LodConfig.CLIENT.graphics.qualityOption.drawResolution.get().detailLevel); + maxDistance = LodConfig.CLIENT.graphics.qualityOption.lodChunkRenderDistance.get() * 16 * 8; + } + + public static int baseDistanceFunction(int detail) + { + if (detail <= minGenDetail) + return minDistance; + if (detail >= maxDetail) + return maxDistance; + + if (LodConfig.CLIENT.graphics.advancedGraphicsOption.alwaysDrawAtMaxQuality.get()) + return detail * 0x10000; //if you want more you are doing wrong + + int distanceUnit = LodConfig.CLIENT.graphics.qualityOption.horizontalScale.get().distanceUnit; + if (LodConfig.CLIENT.graphics.qualityOption.horizontalQuality.get() == HorizontalQuality.LOWEST) + return (detail * distanceUnit); + else + { + double base = LodConfig.CLIENT.graphics.qualityOption.horizontalQuality.get().quadraticBase; + return (int) (Math.pow(base, detail) * distanceUnit); + } + } + + public static int getDrawDistanceFromDetail(int detail) + { + return baseDistanceFunction(detail); + } + + public static byte baseInverseFunction(int distance, byte minDetail, boolean useRenderMinDistance) + { + int detail; + if (distance == 0 + || (distance < minDetailDistance && useRenderMinDistance) + || LodConfig.CLIENT.graphics.advancedGraphicsOption.alwaysDrawAtMaxQuality.get()) + return minDetail; + int distanceUnit = LodConfig.CLIENT.graphics.qualityOption.horizontalScale.get().distanceUnit; + if (LodConfig.CLIENT.graphics.qualityOption.horizontalQuality.get() == HorizontalQuality.LOWEST) + detail = (byte) distance / distanceUnit; + else + { + double base = LodConfig.CLIENT.graphics.qualityOption.horizontalQuality.get().quadraticBase; + double logBase = Math.log(base); + //noinspection IntegerDivisionInFloatingPointContext + detail = (byte) (Math.log(distance / distanceUnit) / logBase); + } + return (byte) LodUtil.clamp(minDetail, detail, maxDetail - 1); + } + + public static byte getDrawDetailFromDistance(int distance) + { + return baseInverseFunction(distance, minDrawDetail, false); + } + + public static byte getGenerationDetailFromDistance(int distance) + { + return baseInverseFunction((int) (distance * genMultiplier), minGenDetail, true); + } + + public static byte getTreeCutDetailFromDistance(int distance) + { + return baseInverseFunction((int) (distance * treeCutMultiplier), minGenDetail, true); + } + + public static byte getTreeGenDetailFromDistance(int distance) + { + return baseInverseFunction((int) (distance * treeGenMultiplier), minGenDetail, true); + } + + public static DistanceGenerationMode getDistanceGenerationMode(int detail) + { + return LodConfig.CLIENT.worldGenerator.distanceGenerationMode.get(); + } + + public static byte getLodDrawDetail(int detail) + { + if (detail < minDrawDetail) + return (byte) minDrawDetail; + else + return (byte) detail; + } + + public static HorizontalResolution getLodGenDetail(int detail) + { + if (detail < minGenDetail) + return lodGenDetails[minGenDetail]; + else + return lodGenDetails[detail]; + } + + + public static byte getCutLodDetail(int detail) + { + if (detail < minGenDetail) + return lodGenDetails[minGenDetail].detailLevel; + else if (detail == maxDetail) + return LodUtil.REGION_DETAIL_LEVEL; + else + return lodGenDetails[detail].detailLevel; + } + + public static int getMaxVerticalData(int detail) + { + return LodConfig.CLIENT.graphics.qualityOption.verticalQuality.get().maxVerticalData[LodUtil.clamp(minGenDetail, detail, LodUtil.REGION_DETAIL_LEVEL)]; + } + +} diff --git a/src/main/java/com/seibel/lod/util/LevelPosUtil.java b/src/main/java/com/seibel/lod/util/LevelPosUtil.java new file mode 100644 index 000000000..11cbfc4f4 --- /dev/null +++ b/src/main/java/com/seibel/lod/util/LevelPosUtil.java @@ -0,0 +1,269 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.util; + +/** + * + * @author Leonardo Amato + * @version ?? + */ +public class LevelPosUtil +{ + public static int[] convert(int[] levelPos, byte newDetailLevel) + { + return convert(getDetailLevel(levelPos), getPosX(levelPos), getPosZ(levelPos), newDetailLevel); + } + + public static int[] convert(byte detailLevel, int posX, int posZ, byte newDetailLevel) + { + int width; + if (newDetailLevel >= detailLevel) + { + width = 1 << (newDetailLevel - detailLevel); + return createLevelPos( + newDetailLevel, + Math.floorDiv(posX, width), + Math.floorDiv(posZ, width)); + } + else + { + width = 1 << (detailLevel - newDetailLevel); + return createLevelPos( + newDetailLevel, + posX * width, + posZ * width); + } + } + + public static int[] createLevelPos(byte detailLevel, int posX, int posZ) + { + return new int[] { detailLevel, posX, posZ }; + } + + public static int convert(byte detailLevel, int pos, byte newDetailLevel) + { + int width; + if (newDetailLevel >= detailLevel) + { + width = 1 << (newDetailLevel - detailLevel); + return Math.floorDiv(pos, width); + } + else + { + width = 1 << (detailLevel - newDetailLevel); + return pos * width; + } + } + + public static int getRegion(byte detailLevel, int pos) + { + return Math.floorDiv(pos, 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel)); + } + + public static int getRegionModule(byte detailLevel, int pos) + { + return Math.floorMod(pos, 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel)); + } + + public static byte getDetailLevel(int[] levelPos) + { + return (byte) levelPos[0]; + } + + public static int getPosX(int[] levelPos) + { + return levelPos[1]; + } + + public static int getPosZ(int[] levelPos) + { + return levelPos[2]; + } + + public static int getDistance(int[] levelPos) + { + return levelPos[3]; + } + + public static int[] getRegionModule(int[] levelPos) + { + return getRegionModule(getDetailLevel(levelPos), getPosX(levelPos), getPosZ(levelPos)); + } + + public static int[] getRegionModule(byte detailLevel, int posX, int posZ) + { + int width = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel); + return createLevelPos( + detailLevel, + Math.floorMod(posX, width), + Math.floorMod(posZ, width)); + } + + public static int[] applyOffset(int[] levelPos, int xOffset, int zOffset) + { + return createLevelPos( + getDetailLevel(levelPos), + getPosX(levelPos) + xOffset, + getPosZ(levelPos) + zOffset); + } + + public static int[] applyLevelOffset(int[] levelPos, byte detailOffset, int xOffset, int zOffset) + { + return createLevelPos( + getDetailLevel(levelPos), + getPosX(levelPos) + xOffset * (1 << detailOffset), + getPosZ(levelPos) + zOffset * (1 << detailOffset)); + } + + public static int getRegionPosX(int[] levelPos) + { + int width = 1 << (LodUtil.REGION_DETAIL_LEVEL - getDetailLevel(levelPos)); + return Math.floorDiv(getPosX(levelPos), width); + } + + public static int getRegionPosZ(int[] levelPos) + { + int width = 1 << (LodUtil.REGION_DETAIL_LEVEL - getDetailLevel(levelPos)); + return Math.floorDiv(getPosZ(levelPos), width); + } + + public static int getChunkPos(byte detailLevel, int pos) + { + return convert(detailLevel, pos, LodUtil.CHUNK_DETAIL_LEVEL); + } + + public static int myPow2(int x) + { + return x*x; + } + + public static int maxDistance(byte detailLevel, int posX, int posZ, int playerPosX, int playerPosZ) + { + int width = 1 << detailLevel; + + int startPosX = posX * width; + int startPosZ = posZ * width; + int endPosX = myPow2(playerPosX - startPosX - width); + int endPosZ = myPow2(playerPosZ - startPosZ - width); + startPosX = myPow2(playerPosX - startPosX); + startPosZ = myPow2(playerPosZ - startPosZ); + + int maxDistance = (int) Math.sqrt(startPosX + startPosZ); + maxDistance = Math.max(maxDistance, (int) Math.sqrt(startPosX + endPosZ)); + maxDistance = Math.max(maxDistance, (int) Math.sqrt(endPosX + startPosZ)); + maxDistance = Math.max(maxDistance, (int) Math.sqrt(endPosX + endPosZ)); + + return maxDistance; + } + + public static int maxDistance(byte detailLevel, int posX, int posZ, int playerPosX, int playerPosZ, int xRegion, int zRegion) + { + int width = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel); + int newPosX = xRegion * width + posX; + int newPosZ = zRegion * width + posZ; + return maxDistance(detailLevel, newPosX, newPosZ, playerPosX, playerPosZ); + } + + + public static int minDistance(byte detailLevel, int posX, int posZ, int playerPosX, int playerPosZ) + { + int width = 1 << detailLevel; + + int startPosX = posX * width; + int startPosZ = posZ * width; + int endPosX = startPosX + width; + int endPosZ = startPosZ + width; + + boolean inXArea = playerPosX >= startPosX && playerPosX <= endPosX; + boolean inZArea = playerPosZ >= startPosZ && playerPosZ <= endPosZ; + if (inXArea && inZArea) + return 0; + else if (inXArea) + { + return Math.min( + Math.abs(playerPosZ - startPosZ), + Math.abs(playerPosZ - endPosZ) + ); + } + else if (inZArea) + { + return Math.min( + Math.abs(playerPosX - startPosX), + Math.abs(playerPosX - endPosX) + ); + } + else + { + startPosX = myPow2(playerPosX - startPosX); + startPosZ = myPow2(playerPosZ - startPosZ); + endPosX = myPow2(playerPosX - endPosX); + endPosZ = myPow2(playerPosZ - endPosZ); + + int minDistance = (int) Math.sqrt(startPosX + startPosZ); + minDistance = Math.min(minDistance, (int) Math.sqrt(startPosX + endPosZ)); + minDistance = Math.min(minDistance, (int) Math.sqrt(endPosX + startPosZ)); + minDistance = Math.min(minDistance, (int) Math.sqrt(endPosX + endPosZ)); + return minDistance; + } + } + + public static int minDistance(byte detailLevel, int posX, int posZ, int playerPosX, int playerPosZ, int xRegion, int zRegion) + { + int width = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel); + int newPosX = xRegion * width + posX; + int newPosZ = zRegion * width + posZ; + return minDistance(detailLevel, newPosX, newPosZ, playerPosX, playerPosZ); + } + + public static int compareDistance(int firstDistance, int secondDistance) + { + return Integer.compare( + firstDistance, + secondDistance); + } + + + public static int compareLevelAndDistance(byte firstDetail, int firstDistance, byte secondDetail, int secondDistance) + { + int compareResult = Integer.compare( + secondDetail, + firstDetail); + if (compareResult == 0) + { + compareResult = Integer.compare( + firstDistance, + secondDistance); + } + return compareResult; + } + + @SuppressWarnings("unused") + public static String toString(int[] levelPos) + { + return (getDetailLevel(levelPos) + " " + + getPosX(levelPos) + " " + + getPosZ(levelPos)); + } + + public static String toString(byte detailLevel, int posX, int posZ) + { + return (detailLevel + " " + posX + " " + posZ); + } +} diff --git a/src/main/java/com/seibel/lod/util/LodThreadFactory.java b/src/main/java/com/seibel/lod/util/LodThreadFactory.java new file mode 100644 index 000000000..62369e5bd --- /dev/null +++ b/src/main/java/com/seibel/lod/util/LodThreadFactory.java @@ -0,0 +1,46 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.util; + +import java.util.concurrent.ThreadFactory; + +/** + * Just a simple ThreadFactory to name ExecutorService + * threads, which can be helpful when debugging. + * @author James Seibel + * @version 8-15-2021 + */ +public class LodThreadFactory implements ThreadFactory +{ + public final String threadName; + + + public LodThreadFactory(String newThreadName) + { + threadName = newThreadName + " Thread"; + } + + @Override + public Thread newThread(Runnable r) + { + return new Thread(r, threadName); + } + +} diff --git a/src/main/java/com/seibel/lod/util/LodUtil.java b/src/main/java/com/seibel/lod/util/LodUtil.java new file mode 100644 index 000000000..30e132379 --- /dev/null +++ b/src/main/java/com/seibel/lod/util/LodUtil.java @@ -0,0 +1,499 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.util; + +import java.awt.Color; +import java.io.File; +import java.util.HashSet; + +import com.seibel.lod.builders.bufferBuilding.lodTemplates.Box; +import com.seibel.lod.config.LodConfig; +import com.seibel.lod.enums.HorizontalResolution; +import com.seibel.lod.enums.VanillaOverdraw; +import com.seibel.lod.objects.LodDimension; +import com.seibel.lod.objects.RegionPos; +import com.seibel.lod.wrappers.MinecraftWrapper; + +import net.minecraft.client.multiplayer.ServerData; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher.CompiledChunk; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.client.renderer.vertex.VertexFormat; +import net.minecraft.server.integrated.IntegratedServer; +import net.minecraft.util.Direction; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.DimensionType; +import net.minecraft.world.IWorld; +import net.minecraft.world.chunk.ChunkSection; +import net.minecraft.world.chunk.IChunk; +import net.minecraft.world.gen.Heightmap; +import net.minecraft.world.server.ServerChunkProvider; +import net.minecraft.world.server.ServerWorld; + +/** + * This class holds methods and constants that may be used in multiple places. + * + * @author James Seibel + * @version 10-20-2021 + */ +public class LodUtil +{ + private static final MinecraftWrapper mc = MinecraftWrapper.INSTANCE; + + /** + * Vanilla render distances less than or equal to this will not allow partial + * overdraw. The VanillaOverdraw will either be ALWAYS or NEVER. + */ + public static final int MINIMUM_RENDER_DISTANCE_FOR_PARTIAL_OVERDRAW = 4; + + /** + * Vanilla render distances less than or equal to this will cause the overdraw to + * run at a smaller fraction of the vanilla render distance. + */ + public static final int MINIMUM_RENDER_DISTANCE_FOR_FAR_OVERDRAW = 11; + + + + + /** The maximum number of LODs that can be rendered vertically */ + public static final int MAX_NUMBER_OF_VERTICAL_LODS = 32; + + /** + * alpha used when drawing chunks in debug mode + */ + public static final int DEBUG_ALPHA = 255; // 0 - 255 + public static final Color COLOR_DEBUG_BLACK = new Color(0, 0, 0, DEBUG_ALPHA); + public static final Color COLOR_DEBUG_WHITE = new Color(255, 255, 255, DEBUG_ALPHA); + public static final Color COLOR_INVISIBLE = new Color(0, 0, 0, 0); + + public static final int CEILED_DIMENSION_MAX_RENDER_DISTANCE = 64; // 0 - 255 + + /** + * In order of nearest to farthest:
+ * Red, Orange, Yellow, Green, Cyan, Blue, Magenta, white, gray, black + */ + public static final Color[] DEBUG_DETAIL_LEVEL_COLORS = new Color[] { Color.RED, Color.ORANGE, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE, Color.MAGENTA, Color.WHITE, Color.GRAY, Color.BLACK }; + + + public static final byte DETAIL_OPTIONS = 10; + + /** 512 blocks wide */ + public static final byte REGION_DETAIL_LEVEL = DETAIL_OPTIONS - 1; + /** 16 blocks wide */ + public static final byte CHUNK_DETAIL_LEVEL = 4; + /** 1 block wide */ + public static final byte BLOCK_DETAIL_LEVEL = 0; + + public static final short MAX_VERTICAL_DATA = 4; + + /** + * measured in Blocks
+ * detail level max - 1 + */ + public static final short REGION_WIDTH = 1 << REGION_DETAIL_LEVEL; + /** + * measured in Blocks
+ * detail level 4 + */ + public static final short CHUNK_WIDTH = 16; + /** + * measured in Blocks
+ * detail level 0 + */ + public static final short BLOCK_WIDTH = 1; + + + /** number of chunks wide */ + public static final int REGION_WIDTH_IN_CHUNKS = REGION_WIDTH / CHUNK_WIDTH; + + + /** + * If we ever need to use a heightmap for any reason, use this one. + */ + public static final Heightmap.Type DEFAULT_HEIGHTMAP = Heightmap.Type.WORLD_SURFACE_WG; + + /** + * This regex finds any characters that are invalid for use in a windows + * (and by extension mac and linux) file path + */ + public static final String INVALID_FILE_CHARACTERS_REGEX = "[\\\\/:*?\"<>|]"; + + /** + * 64 MB by default is the maximum amount of memory that + * can be directly allocated.

+ *

+ * I know there are commands to change that amount + * (specifically "-XX:MaxDirectMemorySize"), but + * I have no idea how to access that amount.
+ * So I guess this will be the hard limit for now.

+ *

+ * https://stackoverflow.com/questions/50499238/bytebuffer-allocatedirect-and-xmx + */ + public static final int MAX_ALLOCATABLE_DIRECT_MEMORY = 64 * 1024 * 1024; + + + public static final VertexFormat LOD_VERTEX_FORMAT = DefaultVertexFormats.POSITION_COLOR; + + + + + + /** + * Gets the first valid ServerWorld. + * @return null if there are no ServerWorlds + */ + public static ServerWorld getFirstValidServerWorld() + { + if (mc.hasSinglePlayerServer()) + return null; + + Iterable worlds = mc.getSinglePlayerServer().getAllLevels(); + + for (ServerWorld world : worlds) + return world; + + return null; + } + + /** + * Gets the ServerWorld for the relevant dimension. + * @return null if there is no ServerWorld for the given dimension + */ + public static ServerWorld getServerWorldFromDimension(DimensionType dimension) + { + IntegratedServer server = mc.getSinglePlayerServer(); + if (server == null) + return null; + + Iterable worlds = server.getAllLevels(); + ServerWorld returnWorld = null; + + for (ServerWorld world : worlds) + { + if (world.dimensionType() == dimension) + { + returnWorld = world; + break; + } + } + + return returnWorld; + } + + /** Convert a 2D absolute position into a quad tree relative position. */ + public static RegionPos convertGenericPosToRegionPos(int x, int z, int detailLevel) + { + int relativePosX = Math.floorDiv(x, 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel)); + int relativePosZ = Math.floorDiv(z, 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel)); + + return new RegionPos(relativePosX, relativePosZ); + } + + /** Convert a 2D absolute position into a quad tree relative position. */ + public static int convertLevelPos(int pos, int currentDetailLevel, int targetDetailLevel) + { + return pos / (1 << (targetDetailLevel - currentDetailLevel)); + } + + /** + * Return whether the given chunk + * has any data in it. + */ + public static boolean chunkHasBlockData(IChunk chunk) + { + ChunkSection[] blockStorage = chunk.getSections(); + + for (ChunkSection section : blockStorage) + { + if (section != null && !section.isEmpty()) + return true; + } + + return false; + } + + + /** + * If on single player this will return the name of the user's + * world, if in multiplayer it will return the server name, IP, + * and game version. + */ + public static String getWorldID(IWorld world) + { + if (mc.hasSinglePlayerServer()) + { + // chop off the dimension ID as it is not needed/wanted + String dimId = getDimensionIDFromWorld(world); + + // get the world name + int saveIndex = dimId.indexOf("saves") + 1 + "saves".length(); + int slashIndex = dimId.indexOf(File.separatorChar, saveIndex); + dimId = dimId.substring(saveIndex, slashIndex); + return dimId; + } + else + { + return getServerId(); + } + } + + + /** + * If on single player this will return the name of the user's + * world and the dimensional save folder, if in multiplayer + * it will return the server name, ip, game version, and dimension.
+ *
+ * This can be used to determine where to save files for a given + * dimension. + */ + public static String getDimensionIDFromWorld(IWorld world) + { + if (mc.hasSinglePlayerServer()) + { + // this will return the world save location + // and the dimension folder + + ServerWorld serverWorld = LodUtil.getServerWorldFromDimension(world.dimensionType()); + if (serverWorld == null) + throw new NullPointerException("getDimensionIDFromWorld wasn't able to get the ServerWorld for the dimension " + world.dimensionType().effectsLocation().getPath()); + + ServerChunkProvider provider = serverWorld.getChunkSource(); + if (provider == null) + throw new NullPointerException("getDimensionIDFromWorld wasn't able to get the ServerChunkProvider for the dimension " + world.dimensionType().effectsLocation().getPath()); + + return provider.dataStorage.dataFolder.toString(); + } + else + { + return getServerId() + File.separatorChar + "dim_" + world.dimensionType().effectsLocation().getPath() + File.separatorChar; + } + } + + /** returns the server name, IP and game version. */ + public static String getServerId() + { + ServerData server = mc.getCurrentServer(); + String serverName = server.name.replaceAll(INVALID_FILE_CHARACTERS_REGEX, ""); + String serverIp = server.ip.replaceAll(INVALID_FILE_CHARACTERS_REGEX, ""); + String serverMcVersion = server.version.getString().replaceAll(INVALID_FILE_CHARACTERS_REGEX, ""); + + return serverName + ", IP " + serverIp + ", GameVersion " + serverMcVersion; + } + + + /** Convert a BlockColors int into a Color object */ + public static Color intToColor(int num) + { + int filter = 0b11111111; + + int red = (num >> 16) & filter; + int green = (num >> 8) & filter; + int blue = num & filter; + + return new Color(red, green, blue); + } + + /** Convert a Color into a BlockColors object. */ + public static int colorToInt(Color color) + { + return color.getRGB(); + } + + + /** + * Clamps the given value between the min and max values. + * May behave strangely if min > max. + */ + public static int clamp(int min, int value, int max) + { + return Math.min(max, Math.max(value, min)); + } + + /** + * Clamps the given value between the min and max values. + * May behave strangely if min > max. + */ + public static float clamp(float min, float value, float max) + { + return Math.min(max, Math.max(value, min)); + } + + /** + * Clamps the given value between the min and max values. + * May behave strangely if min > max. + */ + public static double clamp(double min, double value, double max) + { + return Math.min(max, Math.max(value, min)); + } + + /** + * Get a HashSet of all ChunkPos within the normal render distance + * that should not be rendered. + */ + public static HashSet getNearbyLodChunkPosToSkip(LodDimension lodDim, BlockPos playerPos) + { + int chunkRenderDist = mc.getRenderDistance(); + ChunkPos centerChunk = new ChunkPos(playerPos); + + int skipRadius; + VanillaOverdraw overdraw = LodConfig.CLIENT.graphics.advancedGraphicsOption.vanillaOverdraw.get(); + HorizontalResolution drawRes = LodConfig.CLIENT.graphics.qualityOption.drawResolution.get(); + + // apply distance based rules for dynamic overdraw + if (overdraw == VanillaOverdraw.DYNAMIC + && chunkRenderDist <= MINIMUM_RENDER_DISTANCE_FOR_PARTIAL_OVERDRAW) + { + // The vanilla render distance isn't far enough + // for partial skipping to make sense... + if (!lodDim.dimension.hasCeiling() && (drawRes == HorizontalResolution.BLOCK)) + { + // ...and the dimension is open, so we don't have to worry about + // LODs rendering on top of the player, + // and the user is using a high horizontal resolution, + // so the overdraw shouldn't be noticeable + overdraw = VanillaOverdraw.ALWAYS; + } + else + { + // ...but we are underground, so we don't want + // LODs rendering on top of the player, + // Or the user is using a LOW horizontal resolution + // and overdraw would be very noticeable. + overdraw = VanillaOverdraw.NEVER; + } + } + + + // determine the skipping type based + // on the overdraw type + switch (overdraw) + { + case ALWAYS: + // don't skip any positions + return new HashSet<>(); + + case DYNAMIC: + + if (chunkRenderDist > MINIMUM_RENDER_DISTANCE_FOR_PARTIAL_OVERDRAW + && chunkRenderDist <= MINIMUM_RENDER_DISTANCE_FOR_FAR_OVERDRAW) + { + // This is a small render distance (but greater than the minimum partial + // distance), skip positions that are greater than 2/3 the render distance + skipRadius = (int) Math.ceil(chunkRenderDist * (2.0/3.0)); + } + else + { + // This is a large render distance. Skip positions that are greater than + // 4/5ths the render distance + skipRadius = (int) Math.ceil(chunkRenderDist * (4.0 / 5.0)); + } + break; + + default: + case BORDER: + case NEVER: + // skip chunks in render distance that are rendered + // by vanilla minecraft + skipRadius = 0; + break; + } + + + // get the chunks that are going to be rendered by Minecraft + HashSet posToSkip = getRenderedChunks(); + + + // remove everything outside the skipRadius, + // if the skipRadius is being used + if (skipRadius != 0) + { + for (int x = centerChunk.x - chunkRenderDist; x < centerChunk.x + chunkRenderDist; x++) + { + for (int z = centerChunk.z - chunkRenderDist; z < centerChunk.z + chunkRenderDist; z++) + { + if (x <= centerChunk.x - skipRadius || x >= centerChunk.x + skipRadius + || z <= centerChunk.z - skipRadius || z >= centerChunk.z + skipRadius) + posToSkip.remove(new ChunkPos(x, z)); + + } + } + } + return posToSkip; + } + + + /** + * This method returns the ChunkPos of all chunks that Minecraft + * is going to render this frame.

+ *

+ * Note: This isn't perfect. It will return some chunks that are outside + * the clipping plane. (For example, if you are high above the ground some chunks + * will be incorrectly added, even though they are outside render range). + */ + public static HashSet getRenderedChunks() + { + HashSet loadedPos = new HashSet<>(); + + // Wow, those are some long names! + + // go through every RenderInfo to get the compiled chunks + WorldRenderer renderer = mc.getLevelRenderer(); + for (WorldRenderer.LocalRenderInformationContainer worldRenderer$LocalRenderInformationContainer : renderer.renderChunks) + { + CompiledChunk compiledChunk = worldRenderer$LocalRenderInformationContainer.chunk.getCompiledChunk(); + if (!compiledChunk.hasNoRenderableLayers()) + { + // add the ChunkPos for every rendered chunk + BlockPos bpos = worldRenderer$LocalRenderInformationContainer.chunk.getOrigin(); + + loadedPos.add(new ChunkPos(bpos)); + } + } + + + return loadedPos; + } + + /** + * This method find if a given chunk is a border chunk of the renderable ones + * @param vanillaRenderedChunks matrix of the vanilla rendered chunks + * @param x relative (to the matrix) x chunk to check + * @param z relative (to the matrix) z chunk to check + * @return true if and only if the chunk is a border of the renderable chunks + */ + public static boolean isBorderChunk(boolean[][] vanillaRenderedChunks, int x, int z) + { + if (x < 0 || z < 0 || x >= vanillaRenderedChunks.length || z >= vanillaRenderedChunks[0].length) + return false; + int tempX; + int tempZ; + for (Direction direction : Box.ADJ_DIRECTIONS) + { + tempX = x + Box.DIRECTION_NORMAL_MAP.get(direction).getX(); + tempZ = z + Box.DIRECTION_NORMAL_MAP.get(direction).getZ(); + if (vanillaRenderedChunks[x][z] || (!(tempX < 0 || tempZ < 0 || tempX >= vanillaRenderedChunks.length || tempZ >= vanillaRenderedChunks[0].length) + && !vanillaRenderedChunks[tempX][tempZ])) + return true; + } + return false; + } +} diff --git a/src/main/java/com/seibel/lod/util/ThreadMapUtil.java b/src/main/java/com/seibel/lod/util/ThreadMapUtil.java new file mode 100644 index 000000000..bf19076f3 --- /dev/null +++ b/src/main/java/com/seibel/lod/util/ThreadMapUtil.java @@ -0,0 +1,219 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.util; + +import static com.seibel.lod.util.LodUtil.DETAIL_OPTIONS; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import com.seibel.lod.builders.bufferBuilding.lodTemplates.Box; + +import net.minecraft.util.Direction; + +/** + * Holds data used by specific threads so + * the data doesn't have to be recreated every + * time it is needed. + * + * @author Leonardo Amato + * @version 9-25-2021 + */ +public class ThreadMapUtil +{ + public static final ConcurrentMap threadSingleUpdateMap = new ConcurrentHashMap<>(); + public static final ConcurrentMap threadBuilderArrayMap = new ConcurrentHashMap<>(); + public static final ConcurrentMap threadBuilderVerticalArrayMap = new ConcurrentHashMap<>(); + public static final ConcurrentMap threadVerticalAddDataMap = new ConcurrentHashMap<>(); + public static final ConcurrentMap saveContainer = new ConcurrentHashMap<>(); + public static final ConcurrentMap projectionArrayMap = new ConcurrentHashMap<>(); + public static final ConcurrentMap heightAndDepthMap = new ConcurrentHashMap<>(); + public static final ConcurrentMap singleDataToMergeMap = new ConcurrentHashMap<>(); + public static final ConcurrentMap verticalUpdate = new ConcurrentHashMap<>(); + + + //________________________// + // used in BufferBuilder // + //________________________// + + public static final ConcurrentMap adjShadeDisabled = new ConcurrentHashMap<>(); + public static final ConcurrentMap> adjDataMap = new ConcurrentHashMap<>(); + public static final ConcurrentMap boxMap = new ConcurrentHashMap<>(); + + /** returns the array NOT cleared every time */ + public static boolean[] getAdjShadeDisabledArray() + { + if (!adjShadeDisabled.containsKey(Thread.currentThread().getName()) + || (adjShadeDisabled.get(Thread.currentThread().getName()) == null)) + { + adjShadeDisabled.put(Thread.currentThread().getName(), new boolean[Box.DIRECTIONS.length]); + } + Arrays.fill(adjShadeDisabled.get(Thread.currentThread().getName()), false); + return adjShadeDisabled.get(Thread.currentThread().getName()); + } + + /** returns the array NOT cleared every time */ + public static Map getAdjDataArray(int verticalData) + { + if (!adjDataMap.containsKey(Thread.currentThread().getName()) + || (adjDataMap.get(Thread.currentThread().getName()) == null) + || (adjDataMap.get(Thread.currentThread().getName()).get(Direction.NORTH) == null) + || (adjDataMap.get(Thread.currentThread().getName()).get(Direction.NORTH).length != verticalData)) + { + adjDataMap.put(Thread.currentThread().getName(), new HashMap()); + adjDataMap.get(Thread.currentThread().getName()).put(Direction.UP, new long[1]); + adjDataMap.get(Thread.currentThread().getName()).put(Direction.DOWN, new long[1]); + for (Direction direction : Box.ADJ_DIRECTIONS) + adjDataMap.get(Thread.currentThread().getName()).put(direction, new long[verticalData]); + } + else + { + + for (Direction direction : Box.ADJ_DIRECTIONS) + Arrays.fill(adjDataMap.get(Thread.currentThread().getName()).get(direction), DataPointUtil.EMPTY_DATA); + } + return adjDataMap.get(Thread.currentThread().getName()); + } + + public static Box getBox() + { + if (!boxMap.containsKey(Thread.currentThread().getName()) + || (boxMap.get(Thread.currentThread().getName()) == null)) + { + boxMap.put(Thread.currentThread().getName(), new Box()); + } + boxMap.get(Thread.currentThread().getName()).reset(); + return boxMap.get(Thread.currentThread().getName()); + } + + //________________________// + // used in DataPointUtil // + // mergeVerticalData // + //________________________// + + + //________________________// + // used in DataPointUtil // + // mergeSingleData // + //________________________// + + + + /** returns the array filled with 0's */ + public static long[] getBuilderVerticalArray(int detailLevel) + { + if (!threadBuilderVerticalArrayMap.containsKey(Thread.currentThread().getName()) || (threadBuilderVerticalArrayMap.get(Thread.currentThread().getName()) == null)) + { + long[][] array = new long[5][]; + int size; + for (int i = 0; i < 5; i++) + { + size = 1 << i; + array[i] = new long[size * size * (DataPointUtil.worldHeight / 2 + 1)]; + } + threadBuilderVerticalArrayMap.put(Thread.currentThread().getName(), array); + } + Arrays.fill(threadBuilderVerticalArrayMap.get(Thread.currentThread().getName())[detailLevel], 0); + return threadBuilderVerticalArrayMap.get(Thread.currentThread().getName())[detailLevel]; + } + + /** returns the array NOT cleared every time */ + public static byte[] getSaveContainer(int detailLevel) + { + if (!saveContainer.containsKey(Thread.currentThread().getName()) || (saveContainer.get(Thread.currentThread().getName()) == null)) + { + byte[][] array = new byte[DETAIL_OPTIONS][]; + int size = 1; + for (int i = DETAIL_OPTIONS - 1; i >= 0; i--) + { + array[i] = new byte[2 + 8 * size * size * DetailDistanceUtil.getMaxVerticalData(i)]; + size = size << 1; + } + saveContainer.put(Thread.currentThread().getName(), array); + } + //Arrays.fill(threadBuilderVerticalArrayMap.get(Thread.currentThread().getName())[detailLevel], 0); + return saveContainer.get(Thread.currentThread().getName())[detailLevel]; + } + + + /** returns the array filled with 0's */ + public static long[] getVerticalDataArray(int arrayLength) + { + if (!threadVerticalAddDataMap.containsKey(Thread.currentThread().getName()) || (threadVerticalAddDataMap.get(Thread.currentThread().getName()) == null)) + { + threadVerticalAddDataMap.put(Thread.currentThread().getName(), new long[arrayLength]); + } + else + { + Arrays.fill(threadVerticalAddDataMap.get(Thread.currentThread().getName()), 0); + } + return threadVerticalAddDataMap.get(Thread.currentThread().getName()); + } + + + + /** returns the array NOT cleared every time */ + public static short[] getHeightAndDepth(int arrayLength) + { + if (!heightAndDepthMap.containsKey(Thread.currentThread().getName()) || (heightAndDepthMap.get(Thread.currentThread().getName()) == null)) + { + heightAndDepthMap.put(Thread.currentThread().getName(), new short[arrayLength]); + } + return heightAndDepthMap.get(Thread.currentThread().getName()); + } + + + /** returns the array filled with 0's */ + public static long[] getVerticalUpdateArray(int detailLevel) + { + if (!verticalUpdate.containsKey(Thread.currentThread().getName()) || (verticalUpdate.get(Thread.currentThread().getName()) == null)) + { + long[][] array = new long[DETAIL_OPTIONS][]; + for (int i = 1; i < DETAIL_OPTIONS; i++) + array[i] = new long[DetailDistanceUtil.getMaxVerticalData(i - 1) * 4]; + verticalUpdate.put(Thread.currentThread().getName(), array); + } + else + { + Arrays.fill(verticalUpdate.get(Thread.currentThread().getName())[detailLevel], 0); + } + return verticalUpdate.get(Thread.currentThread().getName())[detailLevel]; + } + + /** clears all arrays so they will have to be rebuilt */ + public static void clearMaps() + { + adjShadeDisabled.clear(); + adjDataMap.clear(); + boxMap.clear(); + threadSingleUpdateMap.clear(); + threadBuilderArrayMap.clear(); + threadBuilderVerticalArrayMap.clear(); + threadVerticalAddDataMap.clear(); + saveContainer.clear(); + projectionArrayMap.clear(); + heightAndDepthMap.clear(); + singleDataToMergeMap.clear(); + verticalUpdate.clear(); + } +} diff --git a/src/main/java/com/seibel/lod/wrappers/Block/BlockColorWrapper.java b/src/main/java/com/seibel/lod/wrappers/Block/BlockColorWrapper.java new file mode 100644 index 000000000..ee082fef5 --- /dev/null +++ b/src/main/java/com/seibel/lod/wrappers/Block/BlockColorWrapper.java @@ -0,0 +1,279 @@ +package com.seibel.lod.wrappers.Block; + +import com.seibel.lod.util.ColorUtil; +import com.seibel.lod.wrappers.MinecraftWrapper; +import net.minecraft.block.*; +import net.minecraft.client.renderer.model.BakedQuad; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.util.Direction; +import net.minecraft.util.math.BlockPos; +import net.minecraftforge.client.model.data.ModelDataMap; + +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + + +//This class wraps the minecraft Block class +public class BlockColorWrapper +{ + //set of block which require tint + public static final ConcurrentMap blockColorWrapperMap = new ConcurrentHashMap<>(); + public static final ModelDataMap dataMap = new ModelDataMap.Builder().build(); + public static final BlockPos blockPos = new BlockPos(0,0,0); + public static final Random random = new Random(0); + //public static BlockColourWrapper WATER_COLOR = getBlockColorWrapper(Blocks.WATER); + public static final Direction[] directions = new Direction[] { Direction.UP, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.NORTH, Direction.DOWN }; + + private final Block block; + private int color; + private boolean isColored; + private boolean toTint; + private boolean foliageTint; + private boolean grassTint; + private boolean waterTint; + + + /**Constructor only require for the block instance we are wrapping**/ + public BlockColorWrapper(BlockState blockState, BlockPosWrapper blockPosWrapper) + { + this.block = blockState.getBlock(); + this.color = 0; + this.isColored = true; + this.toTint = false; + this.foliageTint = false; + this.grassTint = false; + this.waterTint = false; + setupColorAndTint(blockState,blockPosWrapper); + //System.out.println(block + " color " + Integer.toHexString(color) + " to tint " + toTint + " folliageTint " + folliageTint + " grassTint " + grassTint + " waterTint " + waterTint); + } + + /** + * this return a wrapper of the block in input + * @param blockState of the block to wrap + */ + static public BlockColorWrapper getBlockColorWrapper(BlockState blockState, BlockPosWrapper blockPosWrapper) + { + //first we check if the block has already been wrapped + if (blockColorWrapperMap.containsKey(blockState.getBlock()) && blockColorWrapperMap.get(blockState.getBlock()) != null) + return blockColorWrapperMap.get(blockState.getBlock()); + + + //if it hasn't been created yet, we create it and save it in the map + BlockColorWrapper blockWrapper = new BlockColorWrapper(blockState, blockPosWrapper); + blockColorWrapperMap.put(blockState.getBlock(), blockWrapper); + + //we return the newly created wrapper + return blockWrapper; + } + + /** + * Generate the color of the given block from its texture + * and store it for later use. + */ + private void setupColorAndTint(BlockState blockState, BlockPosWrapper blockPosWrapper) + { + MinecraftWrapper mc = MinecraftWrapper.INSTANCE; + TextureAtlasSprite texture; + List quads = null; + + boolean isTinted = false; + int listSize = 0; + + // first step is to check if this block has a tinted face + for (Direction direction : directions) + { + quads = mc.getModelManager().getBlockModelShaper().getBlockModel(block.defaultBlockState()).getQuads(blockState, direction, random, dataMap); + listSize = Math.max(listSize, quads.size()); + for (BakedQuad bakedQuad : quads) + { + isTinted |= bakedQuad.isTinted(); + } + } + + //if it contains a tinted face then we store this block in the toTint set + if (isTinted) + this.toTint = true; + + //now we get the first non empty face + for (Direction direction : directions) + { + quads = mc.getModelManager().getBlockModelShaper().getBlockModel(block.defaultBlockState()).getQuads(blockState, direction, random, dataMap); + if (!quads.isEmpty()) + break; + } + + //the quads list is not empty we extract the first one + if (!quads.isEmpty()) + { + isColored = true; + texture = quads.get(0).getSprite(); + } + else + { + isColored = true; + texture = mc.getModelManager().getBlockModelShaper().getTexture(block.defaultBlockState(), mc.getClientWorld(), blockPosWrapper.getBlockPos()); + } + + int count = 0; + int alpha = 0; + int red = 0; + int green = 0; + int blue = 0; + int numberOfGreyPixel = 0; + int tempColor; + int colorMultiplier; + + // generate the block's color + for (int frameIndex = 0; frameIndex < texture.getFrameCount(); frameIndex++) + { + // textures normally use u and v instead of x and y + for (int u = 0; u < texture.getWidth(); u++) + { + for (int v = 0; v < texture.getHeight(); v++) + { + + tempColor = texture.getPixelRGBA(frameIndex, u, v); + + if (ColorUtil.getAlpha(texture.getPixelRGBA(frameIndex, u, v)) == 0) + continue; + + // determine if this pixel is gray + int colorMax = Math.max(Math.max(ColorUtil.getBlue(tempColor), ColorUtil.getGreen(tempColor)), ColorUtil.getRed(tempColor)); + int colorMin = 4 + Math.min(Math.min(ColorUtil.getBlue(tempColor), ColorUtil.getGreen(tempColor)), ColorUtil.getRed(tempColor)); + boolean isGray = colorMax < colorMin; + if (isGray) + numberOfGreyPixel++; + + + // for flowers, weight their non-green color higher + if (block instanceof FlowerBlock && (!(ColorUtil.getGreen(tempColor) > (ColorUtil.getBlue(tempColor) + 30)) || !(ColorUtil.getGreen(tempColor) > (ColorUtil.getRed(tempColor) + 30)))) + colorMultiplier = 5; + else + colorMultiplier = 1; + + + // add to the running averages + count += colorMultiplier; + alpha += ColorUtil.getAlpha(tempColor) * colorMultiplier; + red += ColorUtil.getBlue(tempColor) * colorMultiplier; + green += ColorUtil.getGreen(tempColor) * colorMultiplier; + blue += ColorUtil.getRed(tempColor) * colorMultiplier; + } + } + } + + + if (count == 0) + // this block is entirely transparent + tempColor = 0; + else + { + // determine the average color + alpha /= count; + red /= count; + green /= count; + blue /= count; + tempColor = ColorUtil.rgbToInt(alpha, red, green, blue); + } + + // determine if this block should use the biome color tint + if ((grassInstance() || leavesInstance() || waterIstance()) && (float) numberOfGreyPixel / count > 0.75f) + this.toTint = true; + + // we check which kind of tint we need to apply + this.grassTint = grassInstance() && toTint; + + this.foliageTint = leavesInstance() && toTint; + + this.waterTint = waterIstance() && toTint; + + color = tempColor; + } + + /** determine if the given block should use the biome's grass color */ + private boolean grassInstance() + { + return block instanceof GrassBlock + || block instanceof BushBlock + || block instanceof IGrowable + || block instanceof AbstractPlantBlock + || block instanceof AbstractTopPlantBlock + || block instanceof TallGrassBlock; + } + + /** determine if the given block should use the biome's foliage color */ + private boolean leavesInstance() + { + return block instanceof LeavesBlock + || block == Blocks.VINE + || block == Blocks.SUGAR_CANE; + } + + /** determine if the given block should use the biome's foliage color */ + private boolean waterIstance() + { + return block == Blocks.WATER; + } + +//--------------// +//Colors getters// +//--------------// + + public boolean hasColor() + { + return isColored; + } + + public int getColor() + { + return color; + } + +//------------// +//Tint getters// +//------------// + + + public boolean hasTint() + { + return toTint; + } + + public boolean hasGrassTint() + { + return grassTint; + } + + public boolean hasFolliageTint() + { + return foliageTint; + } + + public boolean hasWaterTint() + { + return waterTint; + } + + + + + @Override public boolean equals(Object o) + { + if (this == o) + return true; + if (!(o instanceof BlockColorWrapper)) + return false; + BlockColorWrapper that = (BlockColorWrapper) o; + return Objects.equals(block, that.block); + } + + @Override public int hashCode() + { + return Objects.hash(block); + } + +} + diff --git a/src/main/java/com/seibel/lod/wrappers/Block/BlockPosWrapper.java b/src/main/java/com/seibel/lod/wrappers/Block/BlockPosWrapper.java new file mode 100644 index 000000000..7332a8a22 --- /dev/null +++ b/src/main/java/com/seibel/lod/wrappers/Block/BlockPosWrapper.java @@ -0,0 +1,64 @@ +package com.seibel.lod.wrappers.Block; + +import com.seibel.lod.util.ColorUtil; +import net.minecraft.block.*; +import net.minecraft.client.renderer.model.BakedQuad; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.util.Direction; +import net.minecraft.util.math.BlockPos; +import net.minecraftforge.client.model.data.ModelDataMap; + +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + + +//This class wraps the minecraft BlockPos.Mutable (and BlockPos) class +public class BlockPosWrapper +{ + private final BlockPos.Mutable blockPos; + + + public BlockPosWrapper() + { + this.blockPos = new BlockPos.Mutable(0,0,0); + } + + public void set(int x, int y, int z) + { + blockPos.set(x, y, z); + } + + public int getX() + { + return blockPos.getX(); + } + + public int getY() + { + return blockPos.getY(); + } + + public int getZ() + { + return blockPos.getZ(); + } + + public BlockPos.Mutable getBlockPos() + { + return blockPos; + } + + @Override public boolean equals(Object o) + { + return blockPos.equals(o); + } + + @Override public int hashCode() + { + return Objects.hash(blockPos); + } + +} diff --git a/src/main/java/com/seibel/lod/wrappers/Block/BlockShapeWrapper.java b/src/main/java/com/seibel/lod/wrappers/Block/BlockShapeWrapper.java new file mode 100644 index 000000000..df3f9e599 --- /dev/null +++ b/src/main/java/com/seibel/lod/wrappers/Block/BlockShapeWrapper.java @@ -0,0 +1,150 @@ + +package com.seibel.lod.wrappers.Block; + +import com.seibel.lod.wrappers.Chunk.ChunkWrapper; +import net.minecraft.block.Block; +import net.minecraft.block.Blocks; +import net.minecraft.block.SixWayBlock; +import net.minecraft.util.math.AxisAlignedBB; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.shapes.VoxelShape; +import net.minecraft.world.IBlockReader; + +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + + +//This class wraps the minecraft Block class +public class BlockShapeWrapper +{ + //set of block which require tint + public static final ConcurrentMap blockShapeWrapperMap = new ConcurrentHashMap<>(); + public static BlockShapeWrapper WATER_SHAPE = new BlockShapeWrapper(); + + private final Block block; + private final boolean toAvoid; + private boolean nonFull; + private boolean noCollision; + + /**Constructor only require for the block instance we are wrapping**/ + public BlockShapeWrapper(Block block, ChunkWrapper chunkWrapper, BlockPosWrapper blockPosWrapper) + { + this.block = block; + this.nonFull = false; + this.noCollision = false; + this.toAvoid = ofBlockToAvoid(); + setupShapes(chunkWrapper, blockPosWrapper); + //System.out.println(block + " non full " + nonFull + " no collision " + noCollision + " to avoid " + toAvoid); + } + + private BlockShapeWrapper() + { + this.block = Blocks.WATER; + this.nonFull = false; + this.noCollision = false; + this.toAvoid = false; + } + + /** + * this return a wrapper of the block in input + * @param block Block object to wrap + */ + static public BlockShapeWrapper getBlockShapeWrapper(Block block, ChunkWrapper chunkWrapper, BlockPosWrapper blockPosWrapper) + { + //first we check if the block has already been wrapped + if (blockShapeWrapperMap.containsKey(block) && blockShapeWrapperMap.get(block) != null) + return blockShapeWrapperMap.get(block); + + + //if it hasn't been created yet, we create it and save it in the map + BlockShapeWrapper blockWrapper = new BlockShapeWrapper(block, chunkWrapper, blockPosWrapper); + blockShapeWrapperMap.put(block, blockWrapper); + + //we return the newly created wrapper + return blockWrapper; + } + + private void setupShapes(ChunkWrapper chunkWrapper, BlockPosWrapper blockPosWrapper) + { + IBlockReader chunk = chunkWrapper.getChunk(); + BlockPos blockPos = blockPosWrapper.getBlockPos(); + boolean noCollisionSetted = false; + boolean nonFullSetted = false; + if (!block.defaultBlockState().getFluidState().isEmpty() || block instanceof SixWayBlock) + { + noCollisionSetted = true; + nonFullSetted = true; + noCollision = false; + nonFull = false; + } + if (!nonFullSetted) + { + VoxelShape voxelShape = block.defaultBlockState().getShape(chunk, blockPos); + + if (!voxelShape.isEmpty()) + { + AxisAlignedBB bbox = voxelShape.bounds(); + double xWidth = (bbox.maxX - bbox.minX); + double yWidth = (bbox.maxY - bbox.minY); + double zWidth = (bbox.maxZ - bbox.minZ); + nonFull = xWidth < 1 && zWidth < 1 && yWidth < 1; + } + else + { + nonFull = false; + } + } + + if (!noCollisionSetted) + { + VoxelShape collisionShape = block.defaultBlockState().getCollisionShape(chunk, blockPos); + noCollision = collisionShape.isEmpty(); + } + } + + public boolean ofBlockToAvoid() + { + return block.equals(Blocks.AIR) + || block.equals(Blocks.CAVE_AIR) + || block.equals(Blocks.BARRIER) + || block.equals(Blocks.VOID_AIR); + } +//-----------------// +//Avoidance getters// +//-----------------// + + + public boolean isNonFull() + { + return nonFull; + } + + public boolean hasNoCollision() + { + return noCollision; + } + + public boolean isToAvoid() + { + return toAvoid; + } + + + + + @Override public boolean equals(Object o) + { + if (this == o) + return true; + if (!(o instanceof BlockShapeWrapper)) + return false; + BlockShapeWrapper that = (BlockShapeWrapper) o; + return Objects.equals(block, that.block); + } + + @Override public int hashCode() + { + return Objects.hash(block); + } +} \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/wrappers/Chunk/ChunkGenerator.java b/src/main/java/com/seibel/lod/wrappers/Chunk/ChunkGenerator.java new file mode 100644 index 000000000..44be855ae --- /dev/null +++ b/src/main/java/com/seibel/lod/wrappers/Chunk/ChunkGenerator.java @@ -0,0 +1,7 @@ +package com.seibel.lod.wrappers.Chunk; + + +//This class will contain all methods usefull to generate the fake ChunkWrapper +public class ChunkGenerator +{ +} diff --git a/src/main/java/com/seibel/lod/wrappers/Chunk/ChunkPosWrapper.java b/src/main/java/com/seibel/lod/wrappers/Chunk/ChunkPosWrapper.java new file mode 100644 index 000000000..ed93576ce --- /dev/null +++ b/src/main/java/com/seibel/lod/wrappers/Chunk/ChunkPosWrapper.java @@ -0,0 +1,64 @@ +package com.seibel.lod.wrappers.Chunk; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; + +import java.util.Objects; + + +//This class wraps the minecraft ChunkPos class +public class ChunkPosWrapper +{ + private final ChunkPos chunkPos; + + public ChunkPosWrapper(ChunkPos chunkPos) + { + this.chunkPos = chunkPos; + } + + public int getX() + { + return chunkPos.x; + } + + public int getZ() + { + return chunkPos.z; + } + + public int getMinBlockX() + { + return chunkPos.getMinBlockX(); + } + + public int getMinBlockZ() + { + return chunkPos.getMinBlockZ(); + } + + public int getRegionX() + { + return chunkPos.getRegionX(); + } + + public int getRegionZ() + { + return chunkPos.getRegionZ(); + } + + public ChunkPos getChunkPos() + { + return chunkPos; + } + + @Override public boolean equals(Object o) + { + return chunkPos.equals(o); + } + + @Override public int hashCode() + { + return Objects.hash(chunkPos); + } + +} diff --git a/src/main/java/com/seibel/lod/wrappers/Chunk/ChunkWrapper.java b/src/main/java/com/seibel/lod/wrappers/Chunk/ChunkWrapper.java new file mode 100644 index 000000000..078778493 --- /dev/null +++ b/src/main/java/com/seibel/lod/wrappers/Chunk/ChunkWrapper.java @@ -0,0 +1,85 @@ +package com.seibel.lod.wrappers.Chunk; + +import com.seibel.lod.util.LodUtil; +import com.seibel.lod.wrappers.Block.BlockColorWrapper; +import com.seibel.lod.wrappers.Block.BlockPosWrapper; +import com.seibel.lod.wrappers.Block.BlockShapeWrapper; +import com.seibel.lod.wrappers.World.BiomeWrapper; +import net.minecraft.block.BlockState; +import net.minecraft.block.ILiquidContainer; +import net.minecraft.block.IWaterLoggable; +import net.minecraft.state.properties.BlockStateProperties; +import net.minecraft.world.chunk.IChunk; + +public class ChunkWrapper +{ + + private final IChunk chunk; + private final ChunkPosWrapper chunkPos; + + public int getHeight(){ + return chunk.getMaxBuildHeight(); + } + + public boolean isPositionInWater(BlockPosWrapper blockPos) + { + BlockState blockState = chunk.getBlockState(blockPos.getBlockPos()); + + //This type of block is always in water + //This type of block could be in water + return ((blockState.getBlock() instanceof ILiquidContainer) && !(blockState.getBlock() instanceof IWaterLoggable)) + || (blockState.getOptionalValue(BlockStateProperties.WATERLOGGED).isPresent() && blockState.getOptionalValue(BlockStateProperties.WATERLOGGED).get()); + } + + public int getHeightMapValue(int xRel, int zRel){ + return chunk.getOrCreateHeightmapUnprimed(LodUtil.DEFAULT_HEIGHTMAP).getFirstAvailable(xRel, zRel); + } + + public BiomeWrapper getBiome(int xRel, int yAbs, int zRel) + { + return BiomeWrapper.getBiomeWrapper(chunk.getBiomes().getNoiseBiome(xRel >> 2, yAbs >> 2, zRel >> 2)); + } + + public BlockColorWrapper getBlockColorWrapper(BlockPosWrapper blockPos) + { + return BlockColorWrapper.getBlockColorWrapper(chunk.getBlockState(blockPos.getBlockPos()),blockPos); + } + + public BlockShapeWrapper getBlockShapeWrapper(BlockPosWrapper blockPos) + { + return BlockShapeWrapper.getBlockShapeWrapper(chunk.getBlockState(blockPos.getBlockPos()).getBlock(), this, blockPos); + } + + public ChunkWrapper(IChunk chunk) + { + this.chunk = chunk; + this.chunkPos = new ChunkPosWrapper(chunk.getPos()); + } + + public IChunk getChunk(){ + return chunk; + } + public ChunkPosWrapper getPos(){ + return chunkPos; + } + + public boolean isLightCorrect(){ + return chunk.isLightCorrect(); + } + + public boolean + isWaterLogged(BlockPosWrapper blockPos) + { + BlockState blockState = chunk.getBlockState(blockPos.getBlockPos()); + + //This type of block is always in water + //This type of block could be in water + return ((blockState.getBlock() instanceof ILiquidContainer) && !(blockState.getBlock() instanceof IWaterLoggable)) + || (blockState.getOptionalValue(BlockStateProperties.WATERLOGGED).isPresent() && blockState.getOptionalValue(BlockStateProperties.WATERLOGGED).get()); + } + + public int getEmittedBrightness(BlockPosWrapper blockPos) + { + return chunk.getLightEmission(blockPos.getBlockPos()); + } +} diff --git a/src/main/java/com/seibel/lod/wrappers/LightMapWrapper.java b/src/main/java/com/seibel/lod/wrappers/LightMapWrapper.java new file mode 100644 index 000000000..9831a5a95 --- /dev/null +++ b/src/main/java/com/seibel/lod/wrappers/LightMapWrapper.java @@ -0,0 +1,19 @@ +package com.seibel.lod.wrappers; + +import net.minecraft.client.renderer.texture.NativeImage; + + +public class LightMapWrapper +{ + static NativeImage lightMap = null; + + public static void setLightMap(NativeImage newlightMap) + { + lightMap = newlightMap; + } + + public static int getLightValue(int skyLight, int blockLight) + { + return lightMap.getPixelRGBA(skyLight, blockLight); + } +} diff --git a/src/main/java/com/seibel/lod/wrappers/MinecraftWrapper.java b/src/main/java/com/seibel/lod/wrappers/MinecraftWrapper.java new file mode 100644 index 000000000..8c7c2fcd6 --- /dev/null +++ b/src/main/java/com/seibel/lod/wrappers/MinecraftWrapper.java @@ -0,0 +1,289 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * 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, version 3. + * + * 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 . + */ + +package com.seibel.lod.wrappers; + +import java.awt.Color; +import java.io.File; + +import com.seibel.lod.ModInfo; +import com.seibel.lod.proxy.ClientProxy; +import com.seibel.lod.util.LodUtil; + +import com.seibel.lod.wrappers.World.WorldWrapper; +import net.minecraft.client.GameSettings; +import net.minecraft.client.MainWindow; +import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.player.ClientPlayerEntity; +import net.minecraft.client.multiplayer.ServerData; +import net.minecraft.client.network.play.ClientPlayNetHandler; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.model.ModelManager; +import net.minecraft.client.renderer.texture.NativeImage; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.crash.CrashReport; +import net.minecraft.entity.Entity; +import net.minecraft.profiler.IProfiler; +import net.minecraft.server.integrated.IntegratedServer; +import net.minecraft.util.Direction; +import net.minecraft.world.DimensionType; +import net.minecraft.world.server.ServerWorld; + +/** + * A singleton that wraps the Minecraft class + * to allow for easier movement between Minecraft versions. + * + * @author James Seibel + * @version 9-16-2021 + */ +public class MinecraftWrapper +{ + public static final MinecraftWrapper INSTANCE = new MinecraftWrapper(); + + private final Minecraft mc = Minecraft.getInstance(); + + /** + * The lightmap for the current: + * Time, dimension, brightness setting, etc. + */ + private NativeImage lightMap = null; + + private MinecraftWrapper() + { + + } + + + + //================// + // helper methods // + //================// + + /** + * This should be called at the beginning of every frame to + * clear any Minecraft data that becomes out of date after a frame.

+ *

+ * LightMaps and other time sensitive objects fall in this category.

+ *

+ * This doesn't affect OpenGL objects in any way. + */ + public void clearFrameObjectCache() + { + lightMap = null; + } + + + + //=================// + // method wrappers // + //=================// + + public float getShading(Direction direction) + { + return mc.level.getShade(Direction.UP, true); + } + + public boolean hasSinglePlayerServer() + { + return mc.hasSingleplayerServer(); + } + + public DimensionType getCurrentDimension() + { + return mc.player.level.dimensionType(); + } + + public String getCurrentDimensionId() + { + return LodUtil.getDimensionIDFromWorld(mc.level); + } + + /** + * This texture changes every frame + */ + public NativeImage getCurrentLightMap() + { + // get the current lightMap if the cache is empty + if (lightMap == null) + { + LightTexture tex = mc.gameRenderer.lightTexture(); + lightMap = tex.lightPixels; + } + return lightMap; + } + + /** + * Returns the color int at the given pixel coordinates + * from the current lightmap. + * @param u x location in texture space + * @param v z location in texture space + */ + public int getColorIntFromLightMap(int u, int v) + { + if (lightMap == null) + { + // make sure the lightMap is up-to-date + getCurrentLightMap(); + } + + return lightMap.getPixelRGBA(u, v); + } + + /** + * Returns the Color at the given pixel coordinates + * from the current lightmap. + * @param u x location in texture space + * @param v z location in texture space + */ + public Color getColorFromLightMap(int u, int v) + { + return LodUtil.intToColor(lightMap.getPixelRGBA(u, v)); + } + + + + + //=============// + // Simple gets // + //=============// + + public ClientPlayerEntity getPlayer() + { + return mc.player; + } + + public GameSettings getOptions() + { + return mc.options; + } + + public ModelManager getModelManager() + { + return mc.getModelManager(); + } + + public ClientWorld getClientWorld() + { + return mc.level; + } + + public WorldWrapper getWrappedClientWorld() + { + return WorldWrapper.getWorldWrapper(mc.level); + } + + public WorldWrapper getWrappedServerWorld() + { + + if (mc.level == null) + return null; + DimensionType dimension = mc.level.dimensionType(); + IntegratedServer server = mc.getSingleplayerServer(); + if (server == null) + return null; + + Iterable worlds = server.getAllLevels(); + ServerWorld returnWorld = null; + + for (ServerWorld world : worlds) + { + if (world.dimensionType() == dimension) + { + returnWorld = world; + break; + } + } + + return WorldWrapper.getWorldWrapper(returnWorld); + } + + /** Measured in chunks */ + public int getRenderDistance() + { + return mc.options.renderDistance; + } + + public File getGameDirectory() + { + return mc.gameDirectory; + } + + public IProfiler getProfiler() + { + return mc.getProfiler(); + } + + public ClientPlayNetHandler getConnection() + { + return mc.getConnection(); + } + + public GameRenderer getGameRenderer() + { + return mc.gameRenderer; + } + + public Entity getCameraEntity() + { + return mc.cameraEntity; + } + + public MainWindow getWindow() + { + return mc.getWindow(); + } + + public float getSkyDarken(float partialTicks) + { + return mc.level.getSkyDarken(partialTicks); + } + + public IntegratedServer getSinglePlayerServer() + { + return mc.getSingleplayerServer(); + } + + public ServerData getCurrentServer() + { + return mc.getCurrentServer(); + } + + public WorldRenderer getLevelRenderer() + { + return mc.levelRenderer; + } + + + /** + * Crashes Minecraft, displaying the given errorMessage

+ * In the following format:
+ * + * The game crashed whilst errorMessage
+ * Error: java.lang.ExceptionClass: exceptionErrorMessage
+ * Exit Code: -1
+ */ + public void crashMinecraft(String errorMessage, Throwable exception) + { + ClientProxy.LOGGER.error(ModInfo.READABLE_NAME + " had the following error: [" + errorMessage + "]. Crashing Minecraft..."); + CrashReport report = new CrashReport(errorMessage, exception); + Minecraft.crash(report); + } +} diff --git a/src/main/java/com/seibel/lod/wrappers/Vertex/BufferBuilderWrapper.java b/src/main/java/com/seibel/lod/wrappers/Vertex/BufferBuilderWrapper.java new file mode 100644 index 000000000..672c484d6 --- /dev/null +++ b/src/main/java/com/seibel/lod/wrappers/Vertex/BufferBuilderWrapper.java @@ -0,0 +1,5 @@ +package com.seibel.lod.wrappers.Vertex; + +public class BufferBuilderWrapper +{ +} diff --git a/src/main/java/com/seibel/lod/wrappers/Vertex/VertexBufferWrapper.java b/src/main/java/com/seibel/lod/wrappers/Vertex/VertexBufferWrapper.java new file mode 100644 index 000000000..c7fa61f00 --- /dev/null +++ b/src/main/java/com/seibel/lod/wrappers/Vertex/VertexBufferWrapper.java @@ -0,0 +1,5 @@ +package com.seibel.lod.wrappers.Vertex; + +public class VertexBufferWrapper +{ +} diff --git a/src/main/java/com/seibel/lod/wrappers/World/BiomeColorWrapper.java b/src/main/java/com/seibel/lod/wrappers/World/BiomeColorWrapper.java new file mode 100644 index 000000000..9ec7b0072 --- /dev/null +++ b/src/main/java/com/seibel/lod/wrappers/World/BiomeColorWrapper.java @@ -0,0 +1,33 @@ +package com.seibel.lod.wrappers.World; + +import com.seibel.lod.util.LodUtil; +import com.seibel.lod.wrappers.Block.BlockPosWrapper; +import net.minecraft.block.Blocks; +import net.minecraft.block.material.MaterialColor; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.biome.BiomeColors; + +import java.awt.*; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + + +public class BiomeColorWrapper +{ + + public static int getGrassColor(WorldWrapper worldWrapper, BlockPosWrapper blockPosWrapper) + { + return BiomeColors.getAverageGrassColor(worldWrapper.getWorld(), blockPosWrapper.getBlockPos()); + } + public static int getWaterColor(WorldWrapper worldWrapper, BlockPosWrapper blockPosWrapper) + { + + return BiomeColors.getAverageWaterColor(worldWrapper.getWorld(), blockPosWrapper.getBlockPos()); + } + public static int getFoliageColor(WorldWrapper worldWrapper, BlockPosWrapper blockPosWrapper) + { + + return BiomeColors.getAverageFoliageColor(worldWrapper.getWorld(), blockPosWrapper.getBlockPos()); + } +} diff --git a/src/main/java/com/seibel/lod/wrappers/World/BiomeWrapper.java b/src/main/java/com/seibel/lod/wrappers/World/BiomeWrapper.java new file mode 100644 index 000000000..05d4552e3 --- /dev/null +++ b/src/main/java/com/seibel/lod/wrappers/World/BiomeWrapper.java @@ -0,0 +1,138 @@ +package com.seibel.lod.wrappers.World; + +import com.seibel.lod.util.ColorUtil; +import com.seibel.lod.util.LodUtil; +import com.seibel.lod.wrappers.Block.BlockColorWrapper; +import com.seibel.lod.wrappers.Block.BlockPosWrapper; +import net.minecraft.block.Blocks; +import net.minecraft.block.material.MaterialColor; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.biome.BiomeColors; + +import java.awt.*; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + + +//This class wraps the minecraft BlockPos.Mutable (and BlockPos) class +public class BiomeWrapper +{ + + public static final ConcurrentMap biomeWrapperMap = new ConcurrentHashMap<>(); + private final Biome biome; + + public BiomeWrapper(Biome biome) + { + this.biome = biome; + } + + static public BiomeWrapper getBiomeWrapper(Biome biome) + { + //first we check if the biome has already been wrapped + if(biomeWrapperMap.containsKey(biome) && biomeWrapperMap.get(biome) != null) + return biomeWrapperMap.get(biome); + + + //if it hasn't been created yet, we create it and save it in the map + BiomeWrapper biomeWrapper = new BiomeWrapper(biome); + biomeWrapperMap.put(biome, biomeWrapper); + + //we return the newly created wrapper + return biomeWrapper; + } + + + /** Returns a color int for the given biome. */ + public int getColorForBiome(int x, int z) + { + int colorInt; + + switch (biome.getBiomeCategory()) + { + + case NETHER: + colorInt = Blocks.NETHERRACK.defaultBlockState().materialColor.col; + break; + + case THEEND: + colorInt = Blocks.END_STONE.defaultBlockState().materialColor.col; + break; + + case BEACH: + case DESERT: + colorInt = Blocks.SAND.defaultBlockState().materialColor.col; + break; + + case EXTREME_HILLS: + colorInt = Blocks.STONE.defaultMaterialColor().col; + break; + + case MUSHROOM: + colorInt = MaterialColor.COLOR_LIGHT_GRAY.col; + break; + + case ICY: + colorInt = Blocks.SNOW.defaultMaterialColor().col; + break; + + case MESA: + colorInt = Blocks.RED_SAND.defaultMaterialColor().col; + break; + + case OCEAN: + case RIVER: + colorInt = biome.getWaterColor(); + break; + + case NONE: + case FOREST: + case TAIGA: + case JUNGLE: + case PLAINS: + case SAVANNA: + case SWAMP: + default: + Color tmp = LodUtil.intToColor(biome.getGrassColor(x, z)); + tmp = tmp.darker(); + colorInt = LodUtil.colorToInt(tmp); + break; + + } + + return colorInt; + } + + public int getGrassTint(int x, int z) + { + return biome.getGrassColor(x, z); + } + + public int getFolliageTint() + { + return biome.getFoliageColor(); + } + + public int getWaterTint() + { + return biome.getWaterColor(); + } + + + @Override public boolean equals(Object o) + { + if (this == o) + return true; + if (!(o instanceof BiomeWrapper)) + return false; + BiomeWrapper that = (BiomeWrapper) o; + return Objects.equals(biome, that.biome); + } + + @Override public int hashCode() + { + return Objects.hash(biome); + } + +} diff --git a/src/main/java/com/seibel/lod/wrappers/World/DimensionTypeWrapper.java b/src/main/java/com/seibel/lod/wrappers/World/DimensionTypeWrapper.java new file mode 100644 index 000000000..32ee894b1 --- /dev/null +++ b/src/main/java/com/seibel/lod/wrappers/World/DimensionTypeWrapper.java @@ -0,0 +1,37 @@ +package com.seibel.lod.wrappers.World; + +import net.minecraft.world.DimensionType; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class DimensionTypeWrapper +{ + private static final ConcurrentMap dimensionTypeWrapperMap = new ConcurrentHashMap<>(); + private final DimensionType dimensionType; + + public DimensionTypeWrapper(DimensionType dimensionType) + { + this.dimensionType = dimensionType; + } + + public static DimensionTypeWrapper getDimensionTypeWrapper(DimensionType dimensionType) + { + //first we check if the biome has already been wrapped + if(dimensionTypeWrapperMap.containsKey(dimensionType) && dimensionTypeWrapperMap.get(dimensionType) != null) + return dimensionTypeWrapperMap.get(dimensionType); + + + //if it hasn't been created yet, we create it and save it in the map + DimensionTypeWrapper dimensionTypeWrapper = new DimensionTypeWrapper(dimensionType); + dimensionTypeWrapperMap.put(dimensionType, dimensionTypeWrapper); + + //we return the newly created wrapper + return dimensionTypeWrapper; + } + + public static void clearMap() + { + dimensionTypeWrapperMap.clear(); + } +} diff --git a/src/main/java/com/seibel/lod/wrappers/World/WorldLightWrapper.java b/src/main/java/com/seibel/lod/wrappers/World/WorldLightWrapper.java new file mode 100644 index 000000000..e3a4483f6 --- /dev/null +++ b/src/main/java/com/seibel/lod/wrappers/World/WorldLightWrapper.java @@ -0,0 +1,7 @@ +package com.seibel.lod.wrappers.World; + + +//We will use this class to get all the light information from the game like skylight, blocklight and light emission; +public class WorldLightWrapper +{ +} diff --git a/src/main/java/com/seibel/lod/wrappers/World/WorldWrapper.java b/src/main/java/com/seibel/lod/wrappers/World/WorldWrapper.java new file mode 100644 index 000000000..c03f08344 --- /dev/null +++ b/src/main/java/com/seibel/lod/wrappers/World/WorldWrapper.java @@ -0,0 +1,79 @@ +package com.seibel.lod.wrappers.World; + +import com.seibel.lod.wrappers.Block.BlockPosWrapper; +import net.minecraft.world.IWorld; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class WorldWrapper +{ + private static final ConcurrentMap worldWrapperMap = new ConcurrentHashMap<>(); + private final IWorld world; + + public WorldWrapper(IWorld world) + { + this.world = world; + } + + + public static WorldWrapper getWorldWrapper(IWorld world) + { + //first we check if the biome has already been wrapped + if(worldWrapperMap.containsKey(world) && worldWrapperMap.get(world) != null) + return worldWrapperMap.get(world); + + + //if it hasn't been created yet, we create it and save it in the map + WorldWrapper worldWrapper = new WorldWrapper(world); + worldWrapperMap.put(world, worldWrapper); + + //we return the newly created wrapper + return worldWrapper; + } + + public static void clearMap() + { + worldWrapperMap.clear(); + } + + public DimensionTypeWrapper getDimensionType() + { + return DimensionTypeWrapper.getDimensionTypeWrapper(world.dimensionType()); + } + + public int getBlockLight(BlockPosWrapper blockPos) + { + return world.getLightEngine().blockEngine.getLightValue(blockPos.getBlockPos()); + } + + public int getSkyLight(BlockPosWrapper blockPos) + { + return world.getLightEngine().skyEngine.getLightValue(blockPos.getBlockPos()); + } + + public BiomeWrapper getBiome(BlockPosWrapper blockPos) + { + return BiomeWrapper.getBiomeWrapper(world.getBiome(blockPos.getBlockPos())); + } + + public IWorld getWorld() + { + return world; + } + + public boolean hasCeiling() + { + return world.dimensionType().hasCeiling(); + } + + public boolean hasSkyLight() + { + return world.dimensionType().hasSkyLight(); + } + + public boolean isEmpty() + { + return world == null; + } +} diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 000000000..81ffcb0c0 --- /dev/null +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,72 @@ +# Note: to update code in eclipse run the "eclipse" command in graldew + + +# used when creating the projection matrix +public net.minecraft.client.renderer.GameRenderer func_215311_a(Lnet/minecraft/client/renderer/ActiveRenderInfo;FZ)D # getFOVModifier +public net.minecraft.client.renderer.GameRenderer field_78529_t # rendererUpdateCount +public net.minecraft.client.renderer.GameRenderer func_228380_a_(Lcom/mojang/blaze3d/matrix/MatrixStack;F)V # hurtCameraEffect +public net.minecraft.client.renderer.GameRenderer func_228383_b_(Lcom/mojang/blaze3d/matrix/MatrixStack;F)V # applyBobbing + +# used when accessing built byteBuffers +public net.minecraft.client.renderer.BufferBuilder field_179001_a # byteBuffer + +# used when determining where to save files too +public net.minecraft.world.storage.DimensionSavedDataManager field_215759_d # folder + +# used when generating LodChunks +public net.minecraft.block.AbstractBlock$AbstractBlockState field_235704_h_ # materialColor + +# used when determining which chunks Vanilla Minecraft is going to render +public net.minecraft.client.renderer.WorldRenderer$LocalRenderInformationContainer +public net.minecraft.client.renderer.WorldRenderer field_72755_R # renderInfos +public net.minecraft.client.renderer.WorldRenderer$LocalRenderInformationContainer field_178036_a # renderChunk + +# used in world generation +public net.minecraft.world.server.ServerWorld field_241106_P_ # structuremanager +public net.minecraft.world.gen.Heightmap func_202267_b(II)I # getDataArrayIndex +public net.minecraft.world.gen.Heightmap func_202272_a(III)V # set +public net.minecraft.world.chunk.Chunk field_76634_f # heightMap +public net.minecraft.world.chunk.Chunk field_76652_q # sections +public net.minecraft.world.chunk.ChunkPrimer field_201661_i # sections +public net.minecraft.world.server.ChunkManager field_219269_w # templateManager +public net.minecraft.world.server.ChunkManager field_219256_j # lightManager +public net.minecraft.world.gen.feature.template.TemplateManager field_186240_a # templates +public net.minecraft.world.biome.Biome field_242424_k # biomeGenerationSettings +public net.minecraft.world.gen.blockstateprovider.WeightedBlockStateProvider field_227406_b_ # weightedStates +public net.minecraft.world.gen.placement.ConfiguredPlacement field_215096_a # decorator +public net.minecraft.world.gen.placement.ConfiguredPlacement field_215097_b # config +public net.minecraft.util.WeightedList field_220658_a # weightedEntries +public net.minecraft.world.gen.feature.FeatureSpread field_242250_b # base +public net.minecraft.world.gen.feature.FeatureSpread field_242251_c # spread +public net.minecraft.world.gen.feature.ConfiguredFeature func_242765_a(Lnet/minecraft/world/ISeedReader;Lnet/minecraft/world/gen/ChunkGenerator;Ljava/util/Random;Lnet/minecraft/util/math/BlockPos;)Z # place +public net.minecraft.world.server.ServerChunkProvider field_217244_j # dataStorage +public net.minecraft.world.lighting.WorldLightManager field_215576_a # blockEngine +public net.minecraft.world.lighting.WorldLightManager field_215577_b # skyEngine +public net.minecraft.world.gen.feature.Feature field_236290_a_ # configuredCodec + +# used for uploading vertex buffers off the render thread +public net.minecraft.client.renderer.vertex.VertexBuffer field_177365_a # id +public net.minecraft.client.renderer.vertex.VertexBuffer field_177363_b # format +public net.minecraft.client.renderer.vertex.VertexBuffer field_177364_c # vertexCount + +# used for accessing the lightmap +public net.minecraft.client.renderer.LightTexture field_205111_b # lightPixels + + +#=====================# +# Examples from Forge # +#=====================# + +# Makes public the IScreenFactory class in ScreenManager +public net.minecraft.client.gui.ScreenManager$IScreenFactory + +# Makes protected and removes the final modifier from 'random' in MinecraftServer +protected-f net.minecraft.server.MinecraftServer field_147146_q #random + +# Makes public the 'createNamedService' method in Util, +# accepting a String and returns an ExecutorService +public net.minecraft.util.Util func_240979_a_(Ljava/lang/String;)Ljava/util/concurrent/ExecutorService; #createNamedService + +# Makes public the 'func_239776_a_' method in UUIDCodec, +# accepting two longs and returning an int[] +public net.minecraft.util.UUIDCodec func_239776_a_(JJ)[I #func_239776_a_ \ No newline at end of file diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml new file mode 100644 index 000000000..ec77a4159 --- /dev/null +++ b/src/main/resources/META-INF/mods.toml @@ -0,0 +1,48 @@ +#// This is an example mods.toml file. It contains the data relating to the loading mods. +#// There are several mandatory fields (#mandatory), and many more that are optional (#optional). +#// The overall format is standard TOML format, v0.5.0. +#// Note that there are a couple of TOML lists in this file. +#// Find more information on toml format here: https://github.com/toml-lang/toml +#// The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader="javafml" #mandatory + +#// A version range to match for said mod loader - for regular FML @Mod it will be the forge version +loaderVersion="[36,)" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions. + +#// The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +#// Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license="GNU GPLv3" + +#// A URL to refer people to when problems occur with this mod +issueTrackerURL="https://gitlab.com/jeseibel/minecraft-lod-mod/-/issues" #optional +#// A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory + +#// The modid of the mod +modId="lod" #mandatory + +#// The version number of the mod - there's a few well known ${} variables useable here or just hardcode it +#//${file.jarVersion} will substitute the value of the Implementation-Version as read from the mod's JAR file metadata +#// see the associated build.gradle script for how to populate this completely automatically during a build +version="a1.5.2" #mandatory + +#// A display name for the mod +displayName="Distant Horizons" #mandatory + +#// A URL to query for updates for this mod. See the JSON update specification https://mcforge.readthedocs.io/en/latest/gettingstarted/autoupdate/ +#//updateJSONURL="https://change.me.example.invalid/updates.json" #optional + +#// A URL for the "homepage" for this mod, displayed in the mod UI +displayURL="https://www.curseforge.com/minecraft/mc-mods/lod-level-of-detail" #optional + +#// A file name (in the root of the mod JAR) containing a logo for display +logoFile="logo.png" #optional + +#// A text field displayed in the mod UI +credits="TechnoVision, Vike, and Darkhax for their modding tutorials." #optional + +#// A text field displayed in the mod UI +authors="James Seibel, Leonardo Amato, and Cola" #optional + +#// The description text for the mod (multi line!) (#mandatory) +description='''This mod generates and renders simplified terrain beyond the normal view distance, at a low performance cost.''' \ No newline at end of file diff --git a/src/main/resources/lod.mixins.json b/src/main/resources/lod.mixins.json new file mode 100644 index 000000000..d56b32f3a --- /dev/null +++ b/src/main/resources/lod.mixins.json @@ -0,0 +1,10 @@ +{ + "required": true, + "package": "com.seibel.lod.mixin", + "compatibilityLevel": "JAVA_8", + "refmap": "lod.refmap.json", + "mixins": [ + "MixinWorldRenderer" + ], + "minVersion": "0.8" +} \ No newline at end of file diff --git a/src/main/resources/logo.png b/src/main/resources/logo.png new file mode 100644 index 000000000..0caf9c6d1 Binary files /dev/null and b/src/main/resources/logo.png differ diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta new file mode 100644 index 000000000..38f884e64 --- /dev/null +++ b/src/main/resources/pack.mcmeta @@ -0,0 +1,7 @@ +{ + "pack": { + "description": "", + "pack_format": 6, + "_comment": "A pack_format of 6 requires json lang files and some texture changes from 1.16.2. Note: we require v6 pack meta for all mods." + } +}