commit 7c753abdb6e240a5622ce2906a4c520579b89737 Author: Spottedleaf Date: Sat Jul 29 16:00:23 2023 -0700 Initial Commit diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..233fb1b --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +patreon: Spottedleaf \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c37caf --- /dev/null +++ b/.gitignore @@ -0,0 +1,118 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Cache of project +.gradletasknamecache + +**/build/ + +# Common working directory +run/ + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..2fb2e74 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,675 @@ +### GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +### Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom +to share and change all versions of a program--to make sure it remains +free software for all its users. We, the Free Software Foundation, use +the GNU General Public License for most of our software; it applies +also to any other work released this way by its authors. You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you +have certain responsibilities if you distribute copies of the +software, or if you modify it: responsibilities to respect the freedom +of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the +manufacturer can do so. This is fundamentally incompatible with the +aim of protecting users' freedom to change the software. The +systematic pattern of such abuse occurs in the area of products for +individuals to use, which is precisely where it is most unacceptable. +Therefore, we have designed this version of the GPL to prohibit the +practice for those products. If such problems arise substantially in +other domains, we stand ready to extend this provision to those +domains in future versions of the GPL, as needed to protect the +freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish +to avoid the special danger that patents applied to a free program +could make it effectively proprietary. To prevent this, the GPL +assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + +### TERMS AND CONDITIONS + +#### 0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds +of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of +an exact copy. The resulting work is called a "modified version" of +the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user +through a computer network, with no transfer of a copy, is not +conveying. + +An interactive user interface displays "Appropriate Legal Notices" to +the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +#### 1. Source Code. + +The "source code" for a work means the preferred form of the work for +making modifications to it. "Object code" means any non-source form of +a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can +regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same +work. + +#### 2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, +without conditions so long as your license otherwise remains in force. +You may convey covered works to others for the sole purpose of having +them make modifications exclusively for you, or provide you with +facilities for running those works, provided that you comply with the +terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for +you must do so exclusively on your behalf, under your direction and +control, on terms that prohibit them from making any copies of your +copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes +it unnecessary. + +#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such +circumvention is effected by exercising rights under this License with +respect to the covered work, and you disclaim any intention to limit +operation or modification of the work as a means of enforcing, against +the work's users, your or third parties' legal rights to forbid +circumvention of technological measures. + +#### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +#### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these +conditions: + +- a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. +- b) The work must carry prominent notices stating that it is + released under this License and any conditions added under + section 7. This requirement modifies the requirement in section 4 + to "keep intact all notices". +- c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +#### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of +sections 4 and 5, provided that you also convey the machine-readable +Corresponding Source under the terms of this License, in one of these +ways: + +- a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. +- b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the Corresponding + Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. +- d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. +- e) Convey the object code using peer-to-peer transmission, + provided you inform other peers where the object code and + Corresponding Source of the work are being offered to the general + public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, +family, or household purposes, or (2) anything designed or sold for +incorporation into a dwelling. In determining whether a product is a +consumer product, doubtful cases shall be resolved in favor of +coverage. For a particular product received by a particular user, +"normally used" refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected +to use, the product. A product is a consumer product regardless of +whether the product has substantial commercial, industrial or +non-consumer uses, unless such uses represent the only significant +mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to +install and execute modified versions of a covered work in that User +Product from a modified version of its Corresponding Source. The +information must suffice to ensure that the continued functioning of +the modified object code is in no case prevented or interfered with +solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or +updates for a work that has been modified or installed by the +recipient, or for the User Product in which it has been modified or +installed. Access to a network may be denied when the modification +itself materially and adversely affects the operation of the network +or violates the rules and protocols for communication across the +network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +#### 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders +of that material) supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, + or requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors + or authors of the material; or +- e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions + of it) with contractual assumptions of liability to the recipient, + for any liability that these contractual assumptions directly + impose on those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; the +above requirements apply either way. + +#### 8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally +terminates your license, and (b) permanently, if the copyright holder +fails to notify you of the violation by some reasonable means prior to +60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +#### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run +a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +#### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +#### 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned +or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you +are a party to an arrangement with a third party that is in the +business of distributing software, under which you make payment to the +third party based on the extent of your activity of conveying the +work, and under which the third party grants, to any of the parties +who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by +you (or copies made from those copies), or (b) primarily for and in +connection with specific products or compilations that contain the +covered work, unless you entered into that arrangement, or that patent +license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +#### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under +this License and any other pertinent obligations, then as a +consequence you may not convey it at all. For example, if you agree to +terms that obligate you to collect a royalty for further conveying +from those to whom you convey the Program, the only way you could +satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +#### 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +#### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in +detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the GNU General Public +License "or any later version" applies to it, you have the option of +following the terms and conditions either of that numbered version or +of any later version published by the Free Software Foundation. If the +Program does not specify a version number of the GNU General Public +License, you may choose any version ever published by the Free +Software Foundation. + +If the Program specifies that a proxy can decide which future versions +of the GNU General Public License can be used, that proxy's public +statement of acceptance of a version permanently authorizes you to +choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +#### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +#### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR +CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT +NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR +LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM +TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +#### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +### How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + +To do so, attach the following notices to the program. It is safest to +attach them to the start of each source file to most effectively state +the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper +mail. + +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands \`show w' and \`show c' should show the +appropriate parts of the General Public License. Of course, your +program's commands might be different; for a GUI interface, you would +use an "about box". + +You should also get your employer (if you work as a programmer) or +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. For more information on this, and how to apply and follow +the GNU GPL, see . + +The GNU General Public License does not permit incorporating your +program into proprietary programs. If your program is a subroutine +library, you may consider it more useful to permit linking proprietary +applications with the library. If this is what you want to do, use the +GNU Lesser General Public License instead of this License. But first, +please read . diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..bfd540a --- /dev/null +++ b/build.gradle @@ -0,0 +1,97 @@ +plugins { + id 'fabric-loom' version '1.2.7' + id 'maven-publish' +} + +/* + * Gets the version name from the latest Git tag + */ +// https://stackoverflow.com/questions/28498688/gradle-script-to-autoversion-and-include-the-commit-hash-in-android +def getGitCommit = { -> + def stdout = new ByteArrayOutputStream() + exec { + commandLine 'git', 'rev-parse', '--short', 'HEAD' + standardOutput = stdout + } + return stdout.toString().trim() +} + +archivesBaseName = project.archives_base_name +version = project.mod_version + "+fabric." + getGitCommit() +group = project.maven_group + +dependencies { + //to change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + //mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + // Fabric API. This is technically optional, but you probably want it anyway. + //modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + + // PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs. + // You may need to force-disable transitiveness on them. +} + +processResources { + inputs.property "version", project.version + + filesMatching("fabric.mod.json") { + expand "version": project.version + } +} + +// ensure that the encoding is set to UTF-8, no matter what the system default is +// this fixes some edge cases with special characters not displaying correctly +// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html +tasks.withType(JavaCompile) { + options.encoding = "UTF-8" +} + +loom { + accessWidenerPath = file("src/main/resources/moonrise.accesswidener") +} + +// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task +// if it is present. +// If you remove this task, sources will not be generated. +java { + // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task + // if it is present. + // If you remove this line, sources will not be generated. + withSourcesJar() + + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} +jar { + from "LICENSE" +} + +// make build reproducible +tasks.withType(AbstractArchiveTask) { + preserveFileTimestamps = false + reproducibleFileOrder = true +} + +// configure the maven publication +publishing { + publications { + mavenJava(MavenPublication) { + // add all the jars that should be included when publishing to maven + artifact(remapJar) { + builtBy remapJar + } + artifact(sourcesJar) { + builtBy remapSourcesJar + } + } + } + + // select the repositories you want to publish to + repositories { + // uncomment to publish to the local maven + // mavenLocal() + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..af8da8d --- /dev/null +++ b/gradle.properties @@ -0,0 +1,12 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx2G +org.gradle.daemon=false +# Fabric Properties +# check these on https://modmuss50.me/fabric.html +minecraft_version=1.20.1 +yarn_mappings=1.20.1+build.2 +loader_version=0.14.21 +# Mod Properties +mod_version=1.0.0 +maven_group=ca.spottedleaf.moonrise +archives_base_name=moonrise diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 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 0000000..1f017e4 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..a69d9cb --- /dev/null +++ b/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${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='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | 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" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +: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 %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..b02216b --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java new file mode 100644 index 0000000..ba7c24b --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java @@ -0,0 +1,1420 @@ +package ca.spottedleaf.concurrentutil.collection; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import java.lang.invoke.VarHandle; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.function.IntFunction; +import java.util.function.Predicate; + +/** + * MT-Safe linked first in first out ordered queue. + * + * This queue should out-perform {@link java.util.concurrent.ConcurrentLinkedQueue} in high-contention reads/writes, and is + * not any slower in lower contention reads/writes. + *

+ * Note that this queue breaks the specification laid out by {@link Collection}, see {@link #preventAdds()} and {@link Collection#add(Object)}. + *

+ *

+ * This queue will only unlink linked nodes through the {@link #peek()} and {@link #poll()} methods, and this is only if + * they are at the head of the queue. + *

+ * @param Type of element in this queue. + */ +public class MultiThreadedQueue implements Queue { + + protected volatile LinkedNode head; /* Always non-null, high chance of being the actual head */ + + protected volatile LinkedNode tail; /* Always non-null, high chance of being the actual tail */ + + /* Note that it is possible to reach head from tail. */ + + /* IMPL NOTE: Leave hashCode and equals to their defaults */ + + protected static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(MultiThreadedQueue.class, "head", LinkedNode.class); + protected static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(MultiThreadedQueue.class, "tail", LinkedNode.class); + + /* head */ + + protected final void setHeadPlain(final LinkedNode newHead) { + HEAD_HANDLE.set(this, newHead); + } + + protected final void setHeadOpaque(final LinkedNode newHead) { + HEAD_HANDLE.setOpaque(this, newHead); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode getHeadPlain() { + return (LinkedNode)HEAD_HANDLE.get(this); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode getHeadOpaque() { + return (LinkedNode)HEAD_HANDLE.getOpaque(this); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode getHeadAcquire() { + return (LinkedNode)HEAD_HANDLE.getAcquire(this); + } + + /* tail */ + + protected final void setTailPlain(final LinkedNode newTail) { + TAIL_HANDLE.set(this, newTail); + } + + protected final void setTailOpaque(final LinkedNode newTail) { + TAIL_HANDLE.setOpaque(this, newTail); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode getTailPlain() { + return (LinkedNode)TAIL_HANDLE.get(this); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode getTailOpaque() { + return (LinkedNode)TAIL_HANDLE.getOpaque(this); + } + + /** + * Constructs a {@code MultiThreadedQueue}, initially empty. + *

+ * The returned object may not be published without synchronization. + *

+ */ + public MultiThreadedQueue() { + final LinkedNode value = new LinkedNode<>(null, null); + this.setHeadPlain(value); + this.setTailPlain(value); + } + + /** + * Constructs a {@code MultiThreadedQueue}, initially containing all elements in the specified {@code collection}. + *

+ * The returned object may not be published without synchronization. + *

+ * @param collection The specified collection. + * @throws NullPointerException If {@code collection} is {@code null} or contains {@code null} elements. + */ + public MultiThreadedQueue(final Iterable collection) { + final Iterator elements = collection.iterator(); + + if (!elements.hasNext()) { + final LinkedNode value = new LinkedNode<>(null, null); + this.setHeadPlain(value); + this.setTailPlain(value); + return; + } + + final LinkedNode head = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null); + LinkedNode tail = head; + + while (elements.hasNext()) { + final LinkedNode next = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null); + tail.setNextPlain(next); + tail = next; + } + + this.setHeadPlain(head); + this.setTailPlain(tail); + } + + /** + * {@inheritDoc} + */ + @Override + public E remove() throws NoSuchElementException { + final E ret = this.poll(); + + if (ret == null) { + throw new NoSuchElementException(); + } + + return ret; + } + + /** + * {@inheritDoc} + *

+ * Contrary to the specification of {@link Collection#add}, this method will fail to add the element to this queue + * and return {@code false} if this queue is add-blocked. + *

+ */ + @Override + public boolean add(final E element) { + return this.offer(element); + } + + /** + * Adds the specified element to the tail of this queue. If this queue is currently add-locked, then the queue is + * released from that lock and this element is added. The unlock operation and addition of the specified + * element is atomic. + * @param element The specified element. + * @return {@code true} if this queue previously allowed additions + */ + public boolean forceAdd(final E element) { + final LinkedNode node = new LinkedNode<>(element, null); + + return !this.forceAppendList(node, node); + } + + /** + * {@inheritDoc} + */ + @Override + public E element() throws NoSuchElementException { + final E ret = this.peek(); + + if (ret == null) { + throw new NoSuchElementException(); + } + + return ret; + } + + /** + * {@inheritDoc} + *

+ * This method may also return {@code false} to indicate an element was not added if this queue is add-blocked. + *

+ */ + @Override + public boolean offer(final E element) { + Validate.notNull(element, "Null element"); + + final LinkedNode node = new LinkedNode<>(element, null); + + return this.appendList(node, node); + } + + /** + * {@inheritDoc} + */ + @Override + public E peek() { + for (LinkedNode head = this.getHeadOpaque(), curr = head;;) { + final LinkedNode next = curr.getNextVolatile(); + final E element = curr.getElementPlain(); /* Likely in sync */ + + if (element != null) { + if (this.getHeadOpaque() == head && curr != head) { + this.setHeadOpaque(curr); + } + return element; + } + + if (next == null || curr == next) { + return null; + } + curr = next; + } + } + + /** + * {@inheritDoc} + */ + @Override + public E poll() { + return this.removeHead(); + } + + /** + * Retrieves and removes the head of this queue if it matches the specified predicate. If this queue is empty + * or the head does not match the predicate, this function returns {@code null}. + *

+ * The predicate may be invoked multiple or no times in this call. + *

+ * @param predicate The specified predicate. + * @return The head if it matches the predicate, or {@code null} if it did not or this queue is empty. + */ + public E pollIf(final Predicate predicate) { + return this.removeHead(Validate.notNull(predicate, "Null predicate")); + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + //noinspection StatementWithEmptyBody + while (this.poll() != null); + } + + /** + * Prevents elements from being added to this queue. Once this is called, any attempt to add to this queue will fail. + *

+ * This function is MT-Safe. + *

+ * @return {@code true} if the queue was modified to prevent additions, {@code false} if it already prevented additions. + */ + public boolean preventAdds() { + final LinkedNode deadEnd = new LinkedNode<>(null, null); + deadEnd.setNextPlain(deadEnd); + + if (!this.appendList(deadEnd, deadEnd)) { + return false; + } + + this.setTailPlain(deadEnd); /* (try to) Ensure tail is set for the following #allowAdds call */ + return true; + } + + /** + * Allows elements to be added to this queue once again. Note that this function has undefined behaviour if + * {@link #preventAdds()} is not called beforehand. The benefit of this function over {@link #tryAllowAdds()} + * is that this function might perform better. + *

+ * This function is not MT-Safe. + *

+ */ + public void allowAdds() { + LinkedNode tail = this.getTailPlain(); + + /* We need to find the tail given the cas on tail isn't atomic (nor volatile) in this.appendList */ + /* Thus it is possible for an outdated tail to be set */ + while (tail != (tail = tail.getNextPlain())) {} + + tail.setNextVolatile(null); + } + + /** + * Tries to allow elements to be added to this queue. Returns {@code true} if the queue was previous add-locked, + * {@code false} otherwise. + *

+ * This function is MT-Safe, however it should not be used with {@link #allowAdds()}. + *

+ * @return {@code true} if the queue was previously add-locked, {@code false} otherwise. + */ + public boolean tryAllowAdds() { + LinkedNode tail = this.getTailPlain(); + + for (int failures = 0;;) { + /* We need to find the tail given the cas on tail isn't atomic (nor volatile) in this.appendList */ + /* Thus it is possible for an outdated tail to be set */ + while (tail != (tail = tail.getNextAcquire())) { + if (tail == null) { + return false; + } + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (tail == (tail = tail.compareExchangeNextVolatile(tail, null))) { + return true; + } + + if (tail == null) { + return false; + } + ++failures; + } + } + + /** + * Atomically adds the specified element to this queue or allows additions to the queue. If additions + * are not allowed, the element is not added. + *

+ * This function is MT-Safe. + *

+ * @param element The specified element. + * @return {@code true} if the queue now allows additions, {@code false} if the element was added. + */ + public boolean addOrAllowAdds(final E element) { + Validate.notNull(element, "Null element"); + int failures = 0; + + final LinkedNode append = new LinkedNode<>(element, null); + + for (LinkedNode currTail = this.getTailOpaque(), curr = currTail;;) { + /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */ + /* It is likely due to a cache miss caused by another write to the next field */ + final LinkedNode next = curr.getNextVolatile(); + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (next == null) { + final LinkedNode compared = curr.compareExchangeNextVolatile(null, append); + + if (compared == null) { + /* Added */ + /* Avoid CASing on tail more than we need to */ + /* CAS to avoid setting an out-of-date tail */ + if (this.getTailOpaque() == currTail) { + this.setTailOpaque(append); + } + return false; // we added + } + + ++failures; + curr = compared; + continue; + } else if (next == curr) { + final LinkedNode compared = curr.compareExchangeNextVolatile(curr, null); + + if (compared == curr) { + return true; // we let additions through + } + + ++failures; + + if (compared != null) { + curr = compared; + } + continue; + } + + if (curr == currTail) { + /* Tail is likely not up-to-date */ + curr = next; + } else { + /* Try to update to tail */ + if (currTail == (currTail = this.getTailOpaque())) { + curr = next; + } else { + curr = currTail; + } + } + } + } + + /** + * Returns whether this queue is currently add-blocked. That is, whether {@link #add(Object)} and friends will return {@code false}. + */ + public boolean isAddBlocked() { + for (LinkedNode tail = this.getTailOpaque();;) { + LinkedNode next = tail.getNextVolatile(); + if (next == null) { + return false; + } + + if (next == tail) { + return true; + } + + tail = next; + } + } + + /** + * Atomically removes the head from this queue if it exists, otherwise prevents additions to this queue if no + * head is removed. + *

+ * This function is MT-Safe. + *

+ * If the queue is already add-blocked and empty then no operation is performed. + * @return {@code null} if the queue is now add-blocked or was previously add-blocked, else returns + * an non-null value which was the previous head of queue. + */ + public E pollOrBlockAdds() { + int failures = 0; + for (LinkedNode head = this.getHeadOpaque(), curr = head;;) { + final E currentVal = curr.getElementVolatile(); + final LinkedNode next = curr.getNextOpaque(); + + if (next == curr) { + return null; /* Additions are already blocked */ + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (currentVal != null) { + if (curr.getAndSetElementVolatile(null) == null) { + ++failures; + continue; + } + + /* "CAS" to avoid setting an out-of-date head */ + if (this.getHeadOpaque() == head) { + this.setHeadOpaque(next != null ? next : curr); + } + + return currentVal; + } + + if (next == null) { + /* Try to update stale head */ + if (curr != head && this.getHeadOpaque() == head) { + this.setHeadOpaque(curr); + } + + final LinkedNode compared = curr.compareExchangeNextVolatile(null, curr); + + if (compared != null) { + // failed to block additions + curr = compared; + ++failures; + continue; + } + + return null; /* We blocked additions */ + } + + if (head == curr) { + /* head is likely not up-to-date */ + curr = next; + } else { + /* Try to update to head */ + if (head == (head = this.getHeadOpaque())) { + curr = next; + } else { + curr = head; + } + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean remove(final Object object) { + Validate.notNull(object, "Null object to remove"); + + for (LinkedNode curr = this.getHeadOpaque();;) { + final LinkedNode next = curr.getNextVolatile(); + final E element = curr.getElementPlain(); /* Likely in sync */ + + if (element != null) { + if ((element == object || element.equals(object)) && curr.getAndSetElementVolatile(null) == element) { + return true; + } + } + + if (next == curr || next == null) { + break; + } + curr = next; + } + + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean removeIf(final Predicate filter) { + Validate.notNull(filter, "Null filter"); + + boolean ret = false; + + for (LinkedNode curr = this.getHeadOpaque();;) { + final LinkedNode next = curr.getNextVolatile(); + final E element = curr.getElementPlain(); /* Likely in sync */ + + if (element != null) { + ret |= filter.test(element) && curr.getAndSetElementVolatile(null) == element; + } + + if (next == null || next == curr) { + break; + } + curr = next; + } + + return ret; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean removeAll(final Collection collection) { + Validate.notNull(collection, "Null collection"); + + boolean ret = false; + + /* Volatile is required to synchronize with the write to the first element */ + for (LinkedNode curr = this.getHeadOpaque();;) { + final LinkedNode next = curr.getNextVolatile(); + final E element = curr.getElementPlain(); /* Likely in sync */ + + if (element != null) { + ret |= collection.contains(element) && curr.getAndSetElementVolatile(null) == element; + } + + if (next == null || next == curr) { + break; + } + curr = next; + } + + return ret; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean retainAll(final Collection collection) { + Validate.notNull(collection, "Null collection"); + + boolean ret = false; + + for (LinkedNode curr = this.getHeadOpaque();;) { + final LinkedNode next = curr.getNextVolatile(); + final E element = curr.getElementPlain(); /* Likely in sync */ + + if (element != null) { + ret |= !collection.contains(element) && curr.getAndSetElementVolatile(null) == element; + } + + if (next == null || next == curr) { + break; + } + curr = next; + } + + return ret; + } + + /** + * {@inheritDoc} + */ + @Override + public Object[] toArray() { + final List ret = new ArrayList<>(); + + for (LinkedNode curr = this.getHeadOpaque();;) { + final LinkedNode next = curr.getNextVolatile(); + final E element = curr.getElementPlain(); /* Likely in sync */ + + if (element != null) { + ret.add(element); + } + + if (next == null || next == curr) { + break; + } + curr = next; + } + + return ret.toArray(); + } + + /** + * {@inheritDoc} + */ + @Override + public T[] toArray(final T[] array) { + final List ret = new ArrayList<>(); + + for (LinkedNode curr = this.getHeadOpaque();;) { + final LinkedNode next = curr.getNextVolatile(); + final E element = curr.getElementPlain(); /* Likely in sync */ + + if (element != null) { + //noinspection unchecked + ret.add((T)element); + } + + if (next == null || next == curr) { + break; + } + curr = next; + } + + return ret.toArray(array); + } + + /** + * {@inheritDoc} + */ + @Override + public T[] toArray(final IntFunction generator) { + Validate.notNull(generator, "Null generator"); + + final List ret = new ArrayList<>(); + + for (LinkedNode curr = this.getHeadOpaque();;) { + final LinkedNode next = curr.getNextVolatile(); + final E element = curr.getElementPlain(); /* Likely in sync */ + + if (element != null) { + //noinspection unchecked + ret.add((T)element); + } + + if (next == null || next == curr) { + break; + } + curr = next; + } + + return ret.toArray(generator); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + + builder.append("MultiThreadedQueue: {elements: {"); + + int deadEntries = 0; + int totalEntries = 0; + int aliveEntries = 0; + + boolean addLocked = false; + + for (LinkedNode curr = this.getHeadOpaque();; ++totalEntries) { + final LinkedNode next = curr.getNextVolatile(); + final E element = curr.getElementPlain(); /* Likely in sync */ + + if (element == null) { + ++deadEntries; + } else { + ++aliveEntries; + } + + if (totalEntries != 0) { + builder.append(", "); + } + + builder.append(totalEntries).append(": \"").append(element).append('"'); + + if (next == null) { + break; + } + if (curr == next) { + addLocked = true; + break; + } + curr = next; + } + + builder.append("}, total_entries: \"").append(totalEntries).append("\", alive_entries: \"").append(aliveEntries) + .append("\", dead_entries:").append(deadEntries).append("\", add_locked: \"").append(addLocked) + .append("\"}"); + + return builder.toString(); + } + + /** + * Adds all elements from the specified collection to this queue. The addition is atomic. + * @param collection The specified collection. + * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or + * {@code false} if the specified collection contains no elements. + */ + @Override + public boolean addAll(final Collection collection) { + return this.addAll((Iterable)collection); + } + + /** + * Adds all elements from the specified iterable object to this queue. The addition is atomic. + * @param iterable The specified iterable object. + * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or + * {@code false} if the specified iterable contains no elements. + */ + public boolean addAll(final Iterable iterable) { + Validate.notNull(iterable, "Null iterable"); + + final Iterator elements = iterable.iterator(); + if (!elements.hasNext()) { + return false; + } + + /* Build a list of nodes to append */ + /* This is an much faster due to the fact that zero additional synchronization is performed */ + + final LinkedNode head = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null); + LinkedNode tail = head; + + while (elements.hasNext()) { + final LinkedNode next = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null); + tail.setNextPlain(next); + tail = next; + } + + return this.appendList(head, tail); + } + + /** + * Adds all of the elements from the specified array to this queue. + * @param items The specified array. + * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or + * {@code false} if the specified array has a length of 0. + */ + public boolean addAll(final E[] items) { + return this.addAll(items, 0, items.length); + } + + /** + * Adds all of the elements from the specified array to this queue. + * @param items The specified array. + * @param off The offset in the array. + * @param len The number of items. + * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or + * {@code false} if the specified array has a length of 0. + */ + public boolean addAll(final E[] items, final int off, final int len) { + Validate.notNull(items, "Items may not be null"); + Validate.arrayBounds(off, len, items.length, "Items array indices out of bounds"); + + if (len == 0) { + return false; + } + + final LinkedNode head = new LinkedNode<>(Validate.notNull(items[off], "Null element"), null); + LinkedNode tail = head; + + for (int i = 1; i < len; ++i) { + final LinkedNode next = new LinkedNode<>(Validate.notNull(items[off + i], "Null element"), null); + tail.setNextPlain(next); + tail = next; + } + + return this.appendList(head, tail); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean containsAll(final Collection collection) { + Validate.notNull(collection, "Null collection"); + + for (final Object element : collection) { + if (!this.contains(element)) { + return false; + } + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator iterator() { + return new LinkedIterator<>(this.getHeadOpaque()); + } + + /** + * {@inheritDoc} + *

+ * Note that this function is computed non-atomically and in O(n) time. The value returned may not be representative of + * the queue in its current state. + *

+ */ + @Override + public int size() { + int size = 0; + + /* Volatile is required to synchronize with the write to the first element */ + for (LinkedNode curr = this.getHeadOpaque();;) { + final LinkedNode next = curr.getNextVolatile(); + final E element = curr.getElementPlain(); /* Likely in sync */ + + if (element != null) { + ++size; + } + + if (next == null || next == curr) { + break; + } + curr = next; + } + + return size; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isEmpty() { + return this.peek() == null; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean contains(final Object object) { + Validate.notNull(object, "Null object"); + + for (LinkedNode curr = this.getHeadOpaque();;) { + final LinkedNode next = curr.getNextVolatile(); + final E element = curr.getElementPlain(); /* Likely in sync */ + + if (element != null && (element == object || element.equals(object))) { + return true; + } + + if (next == null || next == curr) { + break; + } + curr = next; + } + + return false; + } + + /** + * Finds the first element in this queue that matches the predicate. + * @param predicate The predicate to test elements against. + * @return The first element that matched the predicate, {@code null} if none matched. + */ + public E find(final Predicate predicate) { + Validate.notNull(predicate, "Null predicate"); + + for (LinkedNode curr = this.getHeadOpaque();;) { + final LinkedNode next = curr.getNextVolatile(); + final E element = curr.getElementPlain(); /* Likely in sync */ + + if (element != null && predicate.test(element)) { + return element; + } + + if (next == null || next == curr) { + break; + } + curr = next; + } + + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void forEach(final Consumer action) { + Validate.notNull(action, "Null action"); + + for (LinkedNode curr = this.getHeadOpaque();;) { + final LinkedNode next = curr.getNextVolatile(); + final E element = curr.getElementPlain(); /* Likely in sync */ + + if (element != null) { + action.accept(element); + } + + if (next == null || next == curr) { + break; + } + curr = next; + } + } + + // return true if normal addition, false if the queue previously disallowed additions + protected final boolean forceAppendList(final LinkedNode head, final LinkedNode tail) { + int failures = 0; + + for (LinkedNode currTail = this.getTailOpaque(), curr = currTail;;) { + /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */ + /* It is likely due to a cache miss caused by another write to the next field */ + final LinkedNode next = curr.getNextVolatile(); + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (next == null || next == curr) { + final LinkedNode compared = curr.compareExchangeNextVolatile(next, head); + + if (compared == next) { + /* Added */ + /* Avoid CASing on tail more than we need to */ + /* "CAS" to avoid setting an out-of-date tail */ + if (this.getTailOpaque() == currTail) { + this.setTailOpaque(tail); + } + return next != curr; + } + + ++failures; + curr = compared; + continue; + } + + if (curr == currTail) { + /* Tail is likely not up-to-date */ + curr = next; + } else { + /* Try to update to tail */ + if (currTail == (currTail = this.getTailOpaque())) { + curr = next; + } else { + curr = currTail; + } + } + } + } + + // return true if successful, false otherwise + protected final boolean appendList(final LinkedNode head, final LinkedNode tail) { + int failures = 0; + + for (LinkedNode currTail = this.getTailOpaque(), curr = currTail;;) { + /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */ + /* It is likely due to a cache miss caused by another write to the next field */ + final LinkedNode next = curr.getNextVolatile(); + + if (next == curr) { + /* Additions are stopped */ + return false; + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (next == null) { + final LinkedNode compared = curr.compareExchangeNextVolatile(null, head); + + if (compared == null) { + /* Added */ + /* Avoid CASing on tail more than we need to */ + /* CAS to avoid setting an out-of-date tail */ + if (this.getTailOpaque() == currTail) { + this.setTailOpaque(tail); + } + return true; + } + + ++failures; + curr = compared; + continue; + } + + if (curr == currTail) { + /* Tail is likely not up-to-date */ + curr = next; + } else { + /* Try to update to tail */ + if (currTail == (currTail = this.getTailOpaque())) { + curr = next; + } else { + curr = currTail; + } + } + } + } + + protected final E removeHead(final Predicate predicate) { + int failures = 0; + for (LinkedNode head = this.getHeadOpaque(), curr = head;;) { + // volatile here synchronizes-with writes to element + final LinkedNode next = curr.getNextVolatile(); + final E currentVal = curr.getElementPlain(); + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (currentVal != null) { + if (!predicate.test(currentVal)) { + /* Try to update stale head */ + if (curr != head && this.getHeadOpaque() == head) { + this.setHeadOpaque(curr); + } + return null; + } + if (curr.getAndSetElementVolatile(null) == null) { + /* Failed to get head */ + if (curr == (curr = next) || next == null) { + return null; + } + ++failures; + continue; + } + + /* "CAS" to avoid setting an out-of-date head */ + if (this.getHeadOpaque() == head) { + this.setHeadOpaque(next != null ? next : curr); + } + + return currentVal; + } + + if (curr == next || next == null) { + /* Try to update stale head */ + if (curr != head && this.getHeadOpaque() == head) { + this.setHeadOpaque(curr); + } + return null; /* End of queue */ + } + + if (head == curr) { + /* head is likely not up-to-date */ + curr = next; + } else { + /* Try to update to head */ + if (head == (head = this.getHeadOpaque())) { + curr = next; + } else { + curr = head; + } + } + } + } + + protected final E removeHead() { + int failures = 0; + for (LinkedNode head = this.getHeadOpaque(), curr = head;;) { + final LinkedNode next = curr.getNextVolatile(); + final E currentVal = curr.getElementPlain(); + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (currentVal != null) { + if (curr.getAndSetElementVolatile(null) == null) { + /* Failed to get head */ + if (curr == (curr = next) || next == null) { + return null; + } + ++failures; + continue; + } + + /* "CAS" to avoid setting an out-of-date head */ + if (this.getHeadOpaque() == head) { + this.setHeadOpaque(next != null ? next : curr); + } + + return currentVal; + } + + if (curr == next || next == null) { + /* Try to update stale head */ + if (curr != head && this.getHeadOpaque() == head) { + this.setHeadOpaque(curr); + } + return null; /* End of queue */ + } + + if (head == curr) { + /* head is likely not up-to-date */ + curr = next; + } else { + /* Try to update to head */ + if (head == (head = this.getHeadOpaque())) { + curr = next; + } else { + curr = head; + } + } + } + } + + /** + * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should + * be faster than a loop on {@link #poll()}. + *

+ * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()}, + * {@link #clear()}, etc). + * Write operations are safe to be called concurrently. + *

+ * @param consumer The consumer to accept the elements. + * @return The total number of elements drained. + */ + public int drain(final Consumer consumer) { + return this.drain(consumer, false, ConcurrentUtil::rethrow); + } + + /** + * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should + * be faster than a loop on {@link #poll()}. + *

+ * If {@code preventAdds} is {@code true}, then after this function returns the queue is guaranteed to be empty and + * additions to the queue will fail. + *

+ *

+ * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()}, + * {@link #clear()}, etc). + * Write operations are safe to be called concurrently. + *

+ * @param consumer The consumer to accept the elements. + * @param preventAdds Whether to prevent additions to this queue after draining. + * @return The total number of elements drained. + */ + public int drain(final Consumer consumer, final boolean preventAdds) { + return this.drain(consumer, preventAdds, ConcurrentUtil::rethrow); + } + + /** + * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should + * be faster than a loop on {@link #poll()}. + *

+ * If {@code preventAdds} is {@code true}, then after this function returns the queue is guaranteed to be empty and + * additions to the queue will fail. + *

+ *

+ * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()}, + * {@link #clear()}, {@link #remove(Object)} etc). + * Only write operations are safe to be called concurrently. + *

+ * @param consumer The consumer to accept the elements. + * @param preventAdds Whether to prevent additions to this queue after draining. + * @param exceptionHandler Invoked when the consumer raises an exception. + * @return The total number of elements drained. + */ + public int drain(final Consumer consumer, final boolean preventAdds, final Consumer exceptionHandler) { + Validate.notNull(consumer, "Null consumer"); + Validate.notNull(exceptionHandler, "Null exception handler"); + + /* This function assumes proper synchronization is made to ensure drain and no other read function are called concurrently */ + /* This allows plain write usages instead of opaque or higher */ + int total = 0; + + final LinkedNode head = this.getHeadAcquire(); /* Required to synchronize with the write to the first element field */ + LinkedNode curr = head; + + for (;;) { + /* Volatile acquires with the write to the element field */ + final E currentVal = curr.getElementPlain(); + LinkedNode next = curr.getNextVolatile(); + + if (next == curr) { + /* Add-locked nodes always have a null value */ + break; + } + + if (currentVal == null) { + if (next == null) { + if (preventAdds && (next = curr.compareExchangeNextVolatile(null, curr)) != null) { + // failed to prevent adds, continue + curr = next; + continue; + } else { + // we're done here + break; + } + } + curr = next; + continue; + } + + try { + consumer.accept(currentVal); + } catch (final Exception ex) { + this.setHeadOpaque(next != null ? next : curr); /* Avoid perf penalty (of reiterating) if the exception handler decides to re-throw */ + curr.setElementOpaque(null); /* set here, we might re-throw */ + + exceptionHandler.accept(ex); + } + + curr.setElementOpaque(null); + + ++total; + + if (next == null) { + if (preventAdds && (next = curr.compareExchangeNextVolatile(null, curr)) != null) { + /* Retry with next value */ + curr = next; + continue; + } + break; + } + + curr = next; + } + if (curr != head) { + this.setHeadOpaque(curr); /* While this may be a plain write, eventually publish it for methods such as find. */ + } + return total; + } + + @Override + public Spliterator spliterator() { // TODO implement + return Spliterators.spliterator(this, Spliterator.CONCURRENT | + Spliterator.NONNULL | Spliterator.ORDERED); + } + + protected static final class LinkedNode { + + protected volatile Object element; + protected volatile LinkedNode next; + + protected static final VarHandle ELEMENT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "element", Object.class); + protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "next", LinkedNode.class); + + protected LinkedNode(final Object element, final LinkedNode next) { + ELEMENT_HANDLE.set(this, element); + NEXT_HANDLE.set(this, next); + } + + /* element */ + + @SuppressWarnings("unchecked") + protected final E getElementPlain() { + return (E)ELEMENT_HANDLE.get(this); + } + + @SuppressWarnings("unchecked") + protected final E getElementVolatile() { + return (E)ELEMENT_HANDLE.getVolatile(this); + } + + protected final void setElementPlain(final E update) { + ELEMENT_HANDLE.set(this, (Object)update); + } + + protected final void setElementOpaque(final E update) { + ELEMENT_HANDLE.setOpaque(this, (Object)update); + } + + protected final void setElementVolatile(final E update) { + ELEMENT_HANDLE.setVolatile(this, (Object)update); + } + + @SuppressWarnings("unchecked") + protected final E getAndSetElementVolatile(final E update) { + return (E)ELEMENT_HANDLE.getAndSet(this, update); + } + + @SuppressWarnings("unchecked") + protected final E compareExchangeElementVolatile(final E expect, final E update) { + return (E)ELEMENT_HANDLE.compareAndExchange(this, expect, update); + } + + /* next */ + + @SuppressWarnings("unchecked") + protected final LinkedNode getNextPlain() { + return (LinkedNode)NEXT_HANDLE.get(this); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode getNextOpaque() { + return (LinkedNode)NEXT_HANDLE.getOpaque(this); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode getNextAcquire() { + return (LinkedNode)NEXT_HANDLE.getAcquire(this); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode getNextVolatile() { + return (LinkedNode)NEXT_HANDLE.getVolatile(this); + } + + protected final void setNextPlain(final LinkedNode next) { + NEXT_HANDLE.set(this, next); + } + + protected final void setNextVolatile(final LinkedNode next) { + NEXT_HANDLE.setVolatile(this, next); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode compareExchangeNextVolatile(final LinkedNode expect, final LinkedNode set) { + return (LinkedNode)NEXT_HANDLE.compareAndExchange(this, expect, set); + } + } + + protected static final class LinkedIterator implements Iterator { + + protected LinkedNode curr; /* last returned by next() */ + protected LinkedNode next; /* next to return from next() */ + protected E nextElement; /* cached to avoid a race condition with removing or polling */ + + protected LinkedIterator(final LinkedNode start) { + /* setup nextElement and next */ + for (LinkedNode curr = start;;) { + final LinkedNode next = curr.getNextVolatile(); + + final E element = curr.getElementPlain(); + + if (element != null) { + this.nextElement = element; + this.next = curr; + break; + } + + if (next == null || next == curr) { + break; + } + curr = next; + } + } + + protected final void findNext() { + /* only called if this.nextElement != null, which means this.next != null */ + for (LinkedNode curr = this.next;;) { + final LinkedNode next = curr.getNextVolatile(); + + if (next == null || next == curr) { + break; + } + + final E element = next.getElementPlain(); + + if (element != null) { + this.nextElement = element; + this.curr = this.next; /* this.next will be the value returned from next(), set this.curr for remove() */ + this.next = next; + return; + } + curr = next; + } + + /* out of nodes to iterate */ + /* keep curr for remove() calls */ + this.next = null; + this.nextElement = null; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean hasNext() { + return this.nextElement != null; + } + + /** + * {@inheritDoc} + */ + @Override + public E next() { + final E element = this.nextElement; + + if (element == null) { + throw new NoSuchElementException(); + } + + this.findNext(); + + return element; + } + + /** + * {@inheritDoc} + */ + @Override + public void remove() { + if (this.curr == null) { + throw new IllegalStateException(); + } + + this.curr.setElementVolatile(null); + this.curr = null; + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java new file mode 100644 index 0000000..597659f --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java @@ -0,0 +1,148 @@ +package ca.spottedleaf.concurrentutil.collection; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import java.lang.invoke.VarHandle; +import java.util.ConcurrentModificationException; + +/** + * Single reader thread single writer thread queue. The reader side of the queue is ordered by acquire semantics, + * and the writer side of the queue is ordered by release semantics. + */ +// TODO test +public class SRSWLinkedQueue { + + // always non-null + protected LinkedNode head; + + // always non-null + protected LinkedNode tail; + + /* IMPL NOTE: Leave hashCode and equals to their defaults */ + + public SRSWLinkedQueue() { + final LinkedNode dummy = new LinkedNode<>(null, null); + this.head = this.tail = dummy; + } + + /** + * Must be the reader thread. + * + *

+ * Returns, without removing, the first element of this queue. + *

+ * @return Returns, without removing, the first element of this queue. + */ + public E peekFirst() { + LinkedNode head = this.head; + E ret = head.getElementPlain(); + if (ret == null) { + head = head.getNextAcquire(); + if (head == null) { + // empty + return null; + } + // update head reference for next poll() call + this.head = head; + // guaranteed to be non-null + ret = head.getElementPlain(); + if (ret == null) { + throw new ConcurrentModificationException("Multiple reader threads"); + } + } + + return ret; + } + + /** + * Must be the reader thread. + * + *

+ * Returns and removes the first element of this queue. + *

+ * @return Returns and removes the first element of this queue. + */ + public E poll() { + LinkedNode head = this.head; + E ret = head.getElementPlain(); + if (ret == null) { + head = head.getNextAcquire(); + if (head == null) { + // empty + return null; + } + // guaranteed to be non-null + ret = head.getElementPlain(); + if (ret == null) { + throw new ConcurrentModificationException("Multiple reader threads"); + } + } + + head.setElementPlain(null); + LinkedNode next = head.getNextAcquire(); + this.head = next == null ? head : next; + + return ret; + } + + /** + * Must be the writer thread. + * + *

+ * Adds the element to the end of the queue. + *

+ * + * @throws NullPointerException If the provided element is null + */ + public void addLast(final E element) { + Validate.notNull(element, "Provided element cannot be null"); + final LinkedNode append = new LinkedNode<>(element, null); + + this.tail.setNextRelease(append); + this.tail = append; + } + + protected static final class LinkedNode { + + protected volatile Object element; + protected volatile LinkedNode next; + + protected static final VarHandle ELEMENT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "element", Object.class); + protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "next", LinkedNode.class); + + protected LinkedNode(final Object element, final LinkedNode next) { + ELEMENT_HANDLE.set(this, element); + NEXT_HANDLE.set(this, next); + } + + /* element */ + + @SuppressWarnings("unchecked") + protected final E getElementPlain() { + return (E)ELEMENT_HANDLE.get(this); + } + + protected final void setElementPlain(final E update) { + ELEMENT_HANDLE.set(this, (Object)update); + } + /* next */ + + @SuppressWarnings("unchecked") + protected final LinkedNode getNextPlain() { + return (LinkedNode)NEXT_HANDLE.get(this); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode getNextAcquire() { + return (LinkedNode)NEXT_HANDLE.getAcquire(this); + } + + protected final void setNextPlain(final LinkedNode next) { + NEXT_HANDLE.set(this, next); + } + + protected final void setNextRelease(final LinkedNode next) { + NEXT_HANDLE.setRelease(this, next); + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java new file mode 100644 index 0000000..a1ad330 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java @@ -0,0 +1,98 @@ +package ca.spottedleaf.concurrentutil.completable; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.executor.Cancellable; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; +import java.util.function.BiConsumer; + +public final class Completable { + + private static final Logger LOGGER = LogUtils.getLogger(); + + private final MultiThreadedQueue> waiters = new MultiThreadedQueue<>(); + private T result; + private Throwable throwable; + private volatile boolean completed; + + public boolean isCompleted() { + return this.completed; + } + + /** + * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero + * synchronisation + */ + public T getResult() { + return this.result; + } + + /** + * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero + * synchronisation + */ + public Throwable getThrowable() { + return this.throwable; + } + + public Cancellable addAsynchronousWaiter(final BiConsumer consumer) { + if (this.waiters.add(consumer)) { + return new CancellableImpl(consumer); + } + return null; + } + + private void completeAllWaiters(final T result, final Throwable throwable) { + this.completed = true; + BiConsumer waiter; + while ((waiter = this.waiters.pollOrBlockAdds()) != null) { + this.completeWaiter(waiter, result, throwable); + } + } + + private void completeWaiter(final BiConsumer consumer, final T result, final Throwable throwable) { + try { + consumer.accept(result, throwable); + } catch (final ThreadDeath death) { + throw death; + } catch (final Throwable throwable2) { + LOGGER.error("Failed to complete callback " + ConcurrentUtil.genericToString(consumer), throwable2); + } + } + + public Cancellable addWaiter(final BiConsumer consumer) { + if (this.waiters.add(consumer)) { + return new CancellableImpl(consumer); + } + this.completeWaiter(consumer, this.result, this.throwable); + return new CancellableImpl(consumer); + } + + public void complete(final T result) { + this.result = result; + this.completeAllWaiters(result, null); + } + + public void completeWithThrowable(final Throwable throwable) { + if (throwable == null) { + throw new NullPointerException("Throwable cannot be null"); + } + this.throwable = throwable; + this.completeAllWaiters(null, throwable); + } + + private final class CancellableImpl implements Cancellable { + + private final BiConsumer waiter; + + private CancellableImpl(final BiConsumer waiter) { + this.waiter = waiter; + } + + @Override + public boolean cancel() { + return Completable.this.waiters.remove(this.waiter); + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java new file mode 100644 index 0000000..8c452b0 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java @@ -0,0 +1,198 @@ +package ca.spottedleaf.concurrentutil.executor; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import java.util.function.BooleanSupplier; + +public interface BaseExecutor { + + /** + * Returns whether every task scheduled to this queue has been removed and executed or cancelled. If no tasks have been queued, + * returns {@code true}. + * + * @return {@code true} if all tasks that have been queued have finished executing or no tasks have been queued, {@code false} otherwise. + */ + public default boolean haveAllTasksExecuted() { + // order is important + // if new tasks are scheduled between the reading of these variables, scheduled is guaranteed to be higher - + // so our check fails, and we try again + final long completed = this.getTotalTasksExecuted(); + final long scheduled = this.getTotalTasksScheduled(); + + return completed == scheduled; + } + + /** + * Returns the number of tasks that have been scheduled or execute or are pending to be scheduled. + */ + public long getTotalTasksScheduled(); + + /** + * Returns the number of tasks that have fully been executed. + */ + public long getTotalTasksExecuted(); + + + /** + * Waits until this queue has had all of its tasks executed (NOT removed). See {@link #haveAllTasksExecuted()} + *

+ * This call is most effective after a {@link #shutdown()} call, as the shutdown call guarantees no tasks can + * be executed and the waitUntilAllExecuted call makes sure the queue is empty. Effectively, using shutdown then using + * waitUntilAllExecuted ensures this queue is empty - and most importantly, will remain empty. + *

+ *

+ * This method is not guaranteed to be immediately responsive to queue state, so calls may take significantly more + * time than expected. Effectively, do not rely on this call being fast - even if there are few tasks scheduled. + *

+ *

+ * Note: Interruptions to the the current thread have no effect. Interrupt status is also not affected by this cal. + *

+ * + * @throws IllegalStateException If the current thread is not allowed to wait + */ + public default void waitUntilAllExecuted() throws IllegalStateException { + long failures = 1L; // start at 0.25ms + + while (!this.haveAllTasksExecuted()) { + Thread.yield(); + failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms + } + } + + /** + * Executes the next available task. + *

+ * If there is a task with priority {@link PrioritisedExecutor.Priority#BLOCKING} available, then that such task is executed. + *

+ *

+ * If there is a task with priority {@link PrioritisedExecutor.Priority#IDLE} available then that task is only executed + * when there are no other tasks available with a higher priority. + *

+ *

+ * If there are no tasks that have priority {@link PrioritisedExecutor.Priority#BLOCKING} or {@link PrioritisedExecutor.Priority#IDLE}, then + * this function will be biased to execute tasks that have higher priorities. + *

+ * + * @return {@code true} if a task was executed, {@code false} otherwise + * @throws IllegalStateException If the current thread is not allowed to execute a task + */ + public boolean executeTask() throws IllegalStateException; + + /** + * Executes all queued tasks. + * + * @return {@code true} if a task was executed, {@code false} otherwise + * @throws IllegalStateException If the current thread is not allowed to execute a task + */ + public default boolean executeAll() { + if (!this.executeTask()) { + return false; + } + + while (this.executeTask()); + + return true; + } + + /** + * Waits and executes tasks until the condition returns {@code true}. + *

+ * WARNING: This function is not suitable for waiting until a deadline! + * Use {@link #executeUntil(long)} or {@link #executeConditionally(BooleanSupplier, long)} instead. + *

+ */ + public default void executeConditionally(final BooleanSupplier condition) { + long failures = 0; + while (!condition.getAsBoolean()) { + if (this.executeTask()) { + failures = failures >>> 2; + } else { + failures = ConcurrentUtil.linearLongBackoff(failures, 100_000L, 10_000_000L); // 100us, 10ms + } + } + } + + /** + * Waits and executes tasks until the condition returns {@code true} or {@code System.nanoTime() >= deadline}. + */ + public default void executeConditionally(final BooleanSupplier condition, final long deadline) { + long failures = 0; + // double check deadline; we don't know how expensive the condition is + while ((System.nanoTime() < deadline) && !condition.getAsBoolean() && (System.nanoTime() < deadline)) { + if (this.executeTask()) { + failures = failures >>> 2; + } else { + failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms + } + } + } + + /** + * Waits and executes tasks until {@code System.nanoTime() >= deadline}. + */ + public default void executeUntil(final long deadline) { + long failures = 0; + while (System.nanoTime() < deadline) { + if (this.executeTask()) { + failures = failures >>> 2; + } else { + failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms + } + } + } + + /** + * Prevent further additions to this queue. Attempts to add after this call has completed (potentially during) will + * result in {@link IllegalStateException} being thrown. + *

+ * This operation is atomic with respect to other shutdown calls + *

+ *

+ * After this call has completed, regardless of return value, this queue will be shutdown. + *

+ * + * @return {@code true} if the queue was shutdown, {@code false} if it has shut down already + * @throws UnsupportedOperationException If this queue does not support shutdown + */ + public default boolean shutdown() throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Returns whether this queue has shut down. Effectively, whether new tasks will be rejected - this method + * does not indicate whether all of the tasks scheduled have been executed. + * @return Returns whether this queue has shut down. + */ + public default boolean isShutdown() { + return false; + } + + public static interface BaseTask extends Cancellable { + + /** + * Causes a lazily queued task to become queued or executed + * + * @throws IllegalStateException If the backing queue has shutdown + * @return {@code true} If the task was queued, {@code false} if the task was already queued/cancelled/executed + */ + public boolean queue(); + + /** + * Forces this task to be marked as completed. + * + * @return {@code true} if the task was cancelled, {@code false} if the task has already completed or is being completed. + */ + @Override + public boolean cancel(); + + /** + * Executes this task. This will also mark the task as completing. + *

+ * Exceptions thrown from the runnable will be rethrown. + *

+ * + * @return {@code true} if this task was executed, {@code false} if it was already marked as completed. + */ + public boolean execute(); + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java new file mode 100644 index 0000000..1144905 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java @@ -0,0 +1,14 @@ +package ca.spottedleaf.concurrentutil.executor; + +/** + * Interface specifying that something can be cancelled. + */ +public interface Cancellable { + + /** + * Tries to cancel this task. If the task is in a stage that is too late to be cancelled, then this function + * will return {@code false}. If the task is already cancelled, then this function returns {@code false}. Only + * when this function successfully stops this task from being completed will it return {@code true}. + */ + public boolean cancel(); +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java new file mode 100644 index 0000000..3ce1005 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java @@ -0,0 +1,170 @@ +package ca.spottedleaf.concurrentutil.executor.standard; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import java.lang.invoke.VarHandle; + +public class DelayedPrioritisedTask { + + protected volatile int priority; + protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "priority", int.class); + + protected static final int PRIORITY_SET = Integer.MIN_VALUE >>> 0; + + protected final int getPriorityVolatile() { + return (int)PRIORITY_HANDLE.getVolatile((DelayedPrioritisedTask)this); + } + + protected final int compareAndExchangePriorityVolatile(final int expect, final int update) { + return (int)PRIORITY_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (int)expect, (int)update); + } + + protected final int getAndOrPriorityVolatile(final int val) { + return (int)PRIORITY_HANDLE.getAndBitwiseOr((DelayedPrioritisedTask)this, (int)val); + } + + protected final void setPriorityPlain(final int val) { + PRIORITY_HANDLE.set((DelayedPrioritisedTask)this, (int)val); + } + + protected volatile PrioritisedExecutor.PrioritisedTask task; + protected static final VarHandle TASK_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "task", PrioritisedExecutor.PrioritisedTask.class); + + protected PrioritisedExecutor.PrioritisedTask getTaskPlain() { + return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.get((DelayedPrioritisedTask)this); + } + + protected PrioritisedExecutor.PrioritisedTask getTaskVolatile() { + return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.getVolatile((DelayedPrioritisedTask)this); + } + + protected final PrioritisedExecutor.PrioritisedTask compareAndExchangeTaskVolatile(final PrioritisedExecutor.PrioritisedTask expect, final PrioritisedExecutor.PrioritisedTask update) { + return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (PrioritisedExecutor.PrioritisedTask)expect, (PrioritisedExecutor.PrioritisedTask)update); + } + + public DelayedPrioritisedTask(final PrioritisedExecutor.Priority priority) { + this.setPriorityPlain(priority.priority); + } + + // only public for debugging + public int getPriorityInternal() { + return this.getPriorityVolatile(); + } + + public PrioritisedExecutor.PrioritisedTask getTask() { + return this.getTaskVolatile(); + } + + public void setTask(final PrioritisedExecutor.PrioritisedTask task) { + int priority = this.getPriorityVolatile(); + + if (this.compareAndExchangeTaskVolatile(null, task) != null) { + throw new IllegalStateException("setTask() called twice"); + } + + int failures = 0; + for (;;) { + task.setPriority(PrioritisedExecutor.Priority.getPriority(priority)); + + if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SET))) { + return; + } + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public PrioritisedExecutor.Priority getPriority() { + final int priority = this.getPriorityVolatile(); + if ((priority & PRIORITY_SET) != 0) { + return this.task.getPriority(); + } + + return PrioritisedExecutor.Priority.getPriority(priority); + } + + public void raisePriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_SET) != 0) { + this.getTaskPlain().raisePriority(priority); + return; + } + + if (!priority.isLowerPriority(curr)) { + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public void setPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_SET) != 0) { + this.getTaskPlain().setPriority(priority); + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public void lowerPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_SET) != 0) { + this.getTaskPlain().lowerPriority(priority); + return; + } + + if (!priority.isHigherPriority(curr)) { + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java new file mode 100644 index 0000000..e5d8ff7 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java @@ -0,0 +1,246 @@ +package ca.spottedleaf.concurrentutil.executor.standard; + +import ca.spottedleaf.concurrentutil.executor.BaseExecutor; + +public interface PrioritisedExecutor extends BaseExecutor { + + public static enum Priority { + + /** + * Priority value indicating the task has completed or is being completed. + * This priority cannot be used to schedule tasks. + */ + COMPLETING(-1), + + /** + * Absolute highest priority, should only be used for when a task is blocking a time-critical thread. + */ + BLOCKING(), + + /** + * Should only be used for urgent but not time-critical tasks. + */ + HIGHEST(), + + /** + * Two priorities above normal. + */ + HIGHER(), + + /** + * One priority above normal. + */ + HIGH(), + + /** + * Default priority. + */ + NORMAL(), + + /** + * One priority below normal. + */ + LOW(), + + /** + * Two priorities below normal. + */ + LOWER(), + + /** + * Use for tasks that should eventually execute, but are not needed to. + */ + LOWEST(), + + /** + * Use for tasks that can be delayed indefinitely. + */ + IDLE(); + + // returns whether the priority can be scheduled + public static boolean isValidPriority(final Priority priority) { + return priority != null && priority != Priority.COMPLETING; + } + + // returns the higher priority of the two + public static PrioritisedExecutor.Priority max(final Priority p1, final Priority p2) { + return p1.isHigherOrEqualPriority(p2) ? p1 : p2; + } + + // returns the lower priroity of the two + public static PrioritisedExecutor.Priority min(final Priority p1, final Priority p2) { + return p1.isLowerOrEqualPriority(p2) ? p1 : p2; + } + + public boolean isHigherOrEqualPriority(final Priority than) { + return this.priority <= than.priority; + } + + public boolean isHigherPriority(final Priority than) { + return this.priority < than.priority; + } + + public boolean isLowerOrEqualPriority(final Priority than) { + return this.priority >= than.priority; + } + + public boolean isLowerPriority(final Priority than) { + return this.priority > than.priority; + } + + public boolean isHigherOrEqualPriority(final int than) { + return this.priority <= than; + } + + public boolean isHigherPriority(final int than) { + return this.priority < than; + } + + public boolean isLowerOrEqualPriority(final int than) { + return this.priority >= than; + } + + public boolean isLowerPriority(final int than) { + return this.priority > than; + } + + public static boolean isHigherOrEqualPriority(final int priority, final int than) { + return priority <= than; + } + + public static boolean isHigherPriority(final int priority, final int than) { + return priority < than; + } + + public static boolean isLowerOrEqualPriority(final int priority, final int than) { + return priority >= than; + } + + public static boolean isLowerPriority(final int priority, final int than) { + return priority > than; + } + + static final PrioritisedExecutor.Priority[] PRIORITIES = PrioritisedExecutor.Priority.values(); + + /** includes special priorities */ + public static final int TOTAL_PRIORITIES = PRIORITIES.length; + + public static final int TOTAL_SCHEDULABLE_PRIORITIES = TOTAL_PRIORITIES - 1; + + public static PrioritisedExecutor.Priority getPriority(final int priority) { + return PRIORITIES[priority + 1]; + } + + private static int priorityCounter; + + private static int nextCounter() { + return priorityCounter++; + } + + public final int priority; + + Priority() { + this(nextCounter()); + } + + Priority(final int priority) { + this.priority = priority; + } + } + + /** + * Queues or executes a task at {@link Priority#NORMAL} priority. + * @param task The task to run. + * + * @throws IllegalStateException If this queue has shutdown. + * @throws NullPointerException If the task is null + * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task + * associated with the parameter + */ + public default PrioritisedTask queueRunnable(final Runnable task) { + return this.queueRunnable(task, PrioritisedExecutor.Priority.NORMAL); + } + + /** + * Queues or executes a task. + * + * @param task The task to run. + * @param priority The priority for the task. + * + * @throws IllegalStateException If this queue has shutdown. + * @throws NullPointerException If the task is null + * @throws IllegalArgumentException If the priority is invalid. + * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task + * associated with the parameter + */ + public PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority); + + /** + * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseExecutor.BaseTask#queue()}. + * + * @param task The task to run. + * + * @throws IllegalStateException If this queue has shutdown. + * @throws NullPointerException If the task is null + * @throws IllegalArgumentException If the priority is invalid. + * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks + * @return The prioritised task associated with the parameters + */ + public default PrioritisedExecutor.PrioritisedTask createTask(final Runnable task) { + return this.createTask(task, PrioritisedExecutor.Priority.NORMAL); + } + + /** + * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseExecutor.BaseTask#queue()}. + * + * @param task The task to run. + * @param priority The priority for the task. + * + * @throws IllegalStateException If this queue has shutdown. + * @throws NullPointerException If the task is null + * @throws IllegalArgumentException If the priority is invalid. + * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks + * @return The prioritised task associated with the parameters + */ + public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final PrioritisedExecutor.Priority priority); + + public static interface PrioritisedTask extends BaseTask { + + /** + * Returns the current priority. Note that {@link PrioritisedExecutor.Priority#COMPLETING} will be returned + * if this task is completing or has completed. + */ + public PrioritisedExecutor.Priority getPriority(); + + /** + * Attempts to set this task's priority level to the level specified. + * + * @param priority Specified priority level. + * + * @throws IllegalArgumentException If the priority is invalid + * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue + * this task was scheduled on was shutdown, or if the priority was already at the specified level. + */ + public boolean setPriority(final PrioritisedExecutor.Priority priority); + + /** + * Attempts to raise the priority to the priority level specified. + * + * @param priority Priority specified + * + * @throws IllegalArgumentException If the priority is invalid + * @return {@code false} if the current task is completing, {@code true} if the priority was raised to the specified level or was already at the specified level or higher. + */ + public boolean raisePriority(final PrioritisedExecutor.Priority priority); + + /** + * Attempts to lower the priority to the priority level specified. + * + * @param priority Priority specified + * + * @throws IllegalArgumentException If the priority is invalid + * @return {@code false} if the current task is completing, {@code true} if the priority was lowered to the specified level or was already at the specified level or lower. + */ + public boolean lowerPriority(final PrioritisedExecutor.Priority priority); + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java new file mode 100644 index 0000000..91fe0f7 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java @@ -0,0 +1,297 @@ +package ca.spottedleaf.concurrentutil.executor.standard; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; +import java.lang.invoke.VarHandle; +import java.util.concurrent.locks.LockSupport; + +/** + * Thread which will continuously drain from a specified queue. + *

+ * Note: When using this thread, queue additions to the underlying {@link #queue} are not sufficient to get this thread + * to execute the task. The function {@link #notifyTasks()} must be used after scheduling a task. For expected behaviour + * of task scheduling (thread wakes up after tasks are scheduled), use the methods provided on {@link PrioritisedExecutor} + * methods. + *

+ */ +public class PrioritisedQueueExecutorThread extends Thread implements PrioritisedExecutor { + + private static final Logger LOGGER = LogUtils.getLogger(); + + protected final PrioritisedExecutor queue; + + protected volatile boolean threadShutdown; + + protected static final VarHandle THREAD_PARKED_HANDLE = ConcurrentUtil.getVarHandle(PrioritisedQueueExecutorThread.class, "threadParked", boolean.class); + protected volatile boolean threadParked; + + protected volatile boolean halted; + + protected final long spinWaitTime; + + static final long DEFAULT_SPINWAIT_TIME = (long)(0.1e6);// 0.1ms + + public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue) { + this(queue, DEFAULT_SPINWAIT_TIME); // 0.1ms + } + + public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue, final long spinWaitTime) { // in ns + this.queue = queue; + this.spinWaitTime = spinWaitTime; + } + + @Override + public void run() { + final long spinWaitTime = this.spinWaitTime; + + main_loop: + for (;;) { + this.pollTasks(); + + // spinwait + + final long start = System.nanoTime(); + + for (;;) { + // If we are interrupted for any reason, park() will always return immediately. Clear so that we don't needlessly use cpu in such an event. + Thread.interrupted(); + Thread.yield(); + LockSupport.parkNanos("Spinwaiting on tasks", 10_000L); // 10us + + if (this.pollTasks()) { + // restart loop, found tasks + continue main_loop; + } + + if (this.handleClose()) { + return; // we're done + } + + if ((System.nanoTime() - start) >= spinWaitTime) { + break; + } + } + + if (this.handleClose()) { + return; + } + + this.setThreadParkedVolatile(true); + + // We need to parse here to avoid a race condition where a thread queues a task before we set parked to true + // (i.e it will not notify us) + if (this.pollTasks()) { + this.setThreadParkedVolatile(false); + continue; + } + + if (this.handleClose()) { + return; + } + + // we don't need to check parked before sleeping, but we do need to check parked in a do-while loop + // LockSupport.park() can fail for any reason + while (this.getThreadParkedVolatile()) { + Thread.interrupted(); + LockSupport.park("Waiting on tasks"); + } + } + } + + protected boolean pollTasks() { + boolean ret = false; + + for (;;) { + if (this.halted) { + break; + } + try { + if (!this.queue.executeTask()) { + break; + } + ret = true; + } catch (final ThreadDeath death) { + throw death; // goodbye world... + } catch (final Throwable throwable) { + LOGGER.error("Exception thrown from prioritized runnable task in thread '" + this.getName() + "'", throwable); + } + } + + return ret; + } + + protected boolean handleClose() { + if (this.threadShutdown) { + this.pollTasks(); // this ensures we've emptied the queue + return true; + } + return false; + } + + /** + * Notify this thread that a task has been added to its queue + * @return {@code true} if this thread was waiting for tasks, {@code false} if it is executing tasks + */ + public boolean notifyTasks() { + if (this.getThreadParkedVolatile() && this.exchangeThreadParkedVolatile(false)) { + LockSupport.unpark(this); + return true; + } + return false; + } + + @Override + public PrioritisedTask createTask(final Runnable task, final Priority priority) { + final PrioritisedExecutor.PrioritisedTask queueTask = this.queue.createTask(task, priority); + + // need to override queue() to notify us of tasks + return new PrioritisedTask() { + @Override + public Priority getPriority() { + return queueTask.getPriority(); + } + + @Override + public boolean setPriority(final Priority priority) { + return queueTask.setPriority(priority); + } + + @Override + public boolean raisePriority(final Priority priority) { + return queueTask.raisePriority(priority); + } + + @Override + public boolean lowerPriority(final Priority priority) { + return queueTask.lowerPriority(priority); + } + + @Override + public boolean queue() { + final boolean ret = queueTask.queue(); + if (ret) { + PrioritisedQueueExecutorThread.this.notifyTasks(); + } + return ret; + } + + @Override + public boolean cancel() { + return queueTask.cancel(); + } + + @Override + public boolean execute() { + return queueTask.execute(); + } + }; + } + + @Override + public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority) { + final PrioritisedExecutor.PrioritisedTask ret = this.queue.queueRunnable(task, priority); + + this.notifyTasks(); + + return ret; + } + + @Override + public boolean haveAllTasksExecuted() { + return this.queue.haveAllTasksExecuted(); + } + + @Override + public long getTotalTasksExecuted() { + return this.queue.getTotalTasksExecuted(); + } + + @Override + public long getTotalTasksScheduled() { + return this.queue.getTotalTasksScheduled(); + } + + /** + * {@inheritDoc} + * @throws IllegalStateException If the current thread is {@code this} thread, or the underlying queue throws this exception. + */ + @Override + public void waitUntilAllExecuted() throws IllegalStateException { + if (Thread.currentThread() == this) { + throw new IllegalStateException("Cannot block on our own queue"); + } + this.queue.waitUntilAllExecuted(); + } + + /** + * {@inheritDoc} + * @throws IllegalStateException Always + */ + @Override + public boolean executeTask() throws IllegalStateException { + throw new IllegalStateException(); + } + + /** + * Closes this queue executor's queue. Optionally waits for all tasks in queue to be executed if {@code wait} is true. + *

+ * This function is MT-Safe. + *

+ * @param wait If this call is to wait until the queue is empty and there are no tasks executing in the queue. + * @param killQueue Whether to shutdown this thread's queue + * @return whether this thread shut down the queue + * @see #halt(boolean) + */ + public boolean close(final boolean wait, final boolean killQueue) { + final boolean ret = killQueue && this.queue.shutdown(); + this.threadShutdown = true; + + // force thread to respond to the shutdown + this.setThreadParkedVolatile(false); + LockSupport.unpark(this); + + if (wait) { + this.waitUntilAllExecuted(); + } + + return ret; + } + + + /** + * Causes this thread to exit without draining the queue. To ensure tasks are completed, use {@link #close(boolean, boolean)}. + *

+ * This is not safe to call with {@link #close(boolean, boolean)} if wait = true, in which case + * the waiting thread may block indefinitely. + *

+ *

+ * This function is MT-Safe. + *

+ * @param killQueue Whether to shutdown this thread's queue + * @see #close(boolean, boolean) + */ + public void halt(final boolean killQueue) { + if (killQueue) { + this.queue.shutdown(); + } + this.threadShutdown = true; + this.halted = true; + + // force thread to respond to the shutdown + this.setThreadParkedVolatile(false); + LockSupport.unpark(this); + } + + protected final boolean getThreadParkedVolatile() { + return (boolean)THREAD_PARKED_HANDLE.getVolatile(this); + } + + protected final boolean exchangeThreadParkedVolatile(final boolean value) { + return (boolean)THREAD_PARKED_HANDLE.getAndSet(this, value); + } + + protected final void setThreadParkedVolatile(final boolean value) { + THREAD_PARKED_HANDLE.setVolatile(this, value); + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java new file mode 100644 index 0000000..6e30fe6 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java @@ -0,0 +1,598 @@ +package ca.spottedleaf.concurrentutil.executor.standard; + +import com.mojang.logging.LogUtils; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import org.slf4j.Logger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; + +public final class PrioritisedThreadPool { + + private static final Logger LOGGER = LogUtils.getLogger(); + + protected final PrioritisedThread[] threads; + protected final TreeSet queues = new TreeSet<>(PrioritisedPoolExecutorImpl.comparator()); + protected final String name; + protected final long queueMaxHoldTime; + + protected final ReferenceOpenHashSet nonShutdownQueues = new ReferenceOpenHashSet<>(); + protected final ReferenceOpenHashSet activeQueues = new ReferenceOpenHashSet<>(); + + protected boolean shutdown; + + protected long schedulingIdGenerator; + + protected static final long DEFAULT_QUEUE_HOLD_TIME = (long)(5.0e6); + + public PrioritisedThreadPool(final String name, final int threads) { + this(name, threads, null); + } + + public PrioritisedThreadPool(final String name, final int threads, final BiConsumer threadModifier) { + this(name, threads, threadModifier, DEFAULT_QUEUE_HOLD_TIME); // 5ms + } + + public PrioritisedThreadPool(final String name, final int threads, final BiConsumer threadModifier, + final long queueHoldTime) { // in ns + if (threads <= 0) { + throw new IllegalArgumentException("Thread count must be > 0, not " + threads); + } + if (name == null) { + throw new IllegalArgumentException("Name cannot be null"); + } + this.name = name; + this.queueMaxHoldTime = queueHoldTime; + + this.threads = new PrioritisedThread[threads]; + for (int i = 0; i < threads; ++i) { + this.threads[i] = new PrioritisedThread(this); + + // set default attributes + this.threads[i].setName("Prioritised thread for pool '" + name + "' #" + i); + this.threads[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> { + LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable); + }); + + // let thread modifier override defaults + if (threadModifier != null) { + threadModifier.accept(this.threads[i], Integer.valueOf(i)); + } + + // now the thread can start + this.threads[i].start(); + } + } + + public Thread[] getThreads() { + return Arrays.copyOf(this.threads, this.threads.length, Thread[].class); + } + + public PrioritisedPoolExecutor createExecutor(final String name, final int minParallelism, final int parallelism) { + synchronized (this.nonShutdownQueues) { + if (this.shutdown) { + throw new IllegalStateException("Queue is shutdown: " + this.toString()); + } + final PrioritisedPoolExecutorImpl ret = new PrioritisedPoolExecutorImpl( + this, name, + Math.min(Math.max(1, parallelism), this.threads.length), + Math.min(Math.max(0, minParallelism), this.threads.length) + ); + + this.nonShutdownQueues.add(ret); + + synchronized (this.activeQueues) { + this.activeQueues.add(ret); + } + + return ret; + } + } + + /** + * Prevents creation of new queues, shutdowns all non-shutdown queues if specified + */ + public void halt(final boolean shutdownQueues) { + synchronized (this.nonShutdownQueues) { + this.shutdown = true; + } + if (shutdownQueues) { + final ArrayList queuesToShutdown; + synchronized (this.nonShutdownQueues) { + this.shutdown = true; + queuesToShutdown = new ArrayList<>(this.nonShutdownQueues); + } + + for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) { + queue.shutdown(); + } + } + + + for (final PrioritisedThread thread : this.threads) { + // can't kill queue, queue is null + thread.halt(false); + } + } + + /** + * Waits until all threads in this pool have shutdown, or until the specified time has passed. + * @param msToWait Maximum time to wait. + * @return {@code false} if the maximum time passed, {@code true} otherwise. + */ + public boolean join(final long msToWait) { + try { + return this.join(msToWait, false); + } catch (final InterruptedException ex) { + throw new IllegalStateException(ex); + } + } + + /** + * Waits until all threads in this pool have shutdown, or until the specified time has passed. + * @param msToWait Maximum time to wait. + * @return {@code false} if the maximum time passed, {@code true} otherwise. + * @throws InterruptedException If this thread is interrupted. + */ + public boolean joinInterruptable(final long msToWait) throws InterruptedException { + return this.join(msToWait, true); + } + + protected final boolean join(final long msToWait, final boolean interruptable) throws InterruptedException { + final long nsToWait = msToWait * (1000 * 1000); + final long start = System.nanoTime(); + final long deadline = start + nsToWait; + boolean interrupted = false; + try { + for (final PrioritisedThread thread : this.threads) { + for (;;) { + if (!thread.isAlive()) { + break; + } + final long current = System.nanoTime(); + if (current >= deadline) { + return false; + } + + try { + thread.join(Math.max(1L, (deadline - current) / (1000 * 1000))); + } catch (final InterruptedException ex) { + if (interruptable) { + throw ex; + } + interrupted = true; + } + } + } + + return true; + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + + public void shutdown(final boolean wait) { + final ArrayList queuesToShutdown; + synchronized (this.nonShutdownQueues) { + this.shutdown = true; + queuesToShutdown = new ArrayList<>(this.nonShutdownQueues); + } + + for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) { + queue.shutdown(); + } + + for (final PrioritisedThread thread : this.threads) { + // none of these can be true or else NPE + thread.close(false, false); + } + + if (wait) { + final ArrayList queues; + synchronized (this.activeQueues) { + queues = new ArrayList<>(this.activeQueues); + } + for (final PrioritisedPoolExecutorImpl queue : queues) { + queue.waitUntilAllExecuted(); + } + } + } + + protected static final class PrioritisedThread extends PrioritisedQueueExecutorThread { + + protected final PrioritisedThreadPool pool; + protected final AtomicBoolean alertedHighPriority = new AtomicBoolean(); + + public PrioritisedThread(final PrioritisedThreadPool pool) { + super(null); + this.pool = pool; + } + + public boolean alertHighPriorityExecutor() { + if (!this.notifyTasks()) { + if (!this.alertedHighPriority.get()) { + this.alertedHighPriority.set(true); + } + return false; + } + + return true; + } + + private boolean isAlertedHighPriority() { + return this.alertedHighPriority.get() && this.alertedHighPriority.getAndSet(false); + } + + @Override + protected boolean pollTasks() { + final PrioritisedThreadPool pool = this.pool; + final TreeSet queues = this.pool.queues; + + boolean ret = false; + for (;;) { + if (this.halted) { + break; + } + // try to find a queue + // note that if and ONLY IF the queues set is empty, this means there are no tasks for us to execute. + // so we can only break when it's empty + final PrioritisedPoolExecutorImpl queue; + // select queue + synchronized (queues) { + queue = queues.pollFirst(); + if (queue == null) { + // no tasks to execute + break; + } + + queue.schedulingId = ++pool.schedulingIdGenerator; + // we own this queue now, so increment the executor count + // do we also need to push this queue up for grabs for another executor? + if (++queue.concurrentExecutors < queue.maximumExecutors) { + // re-add to queues + // it's very important this is done in the same synchronised block for polling, as this prevents + // us from possibly later adding a queue that should not exist in the set + queues.add(queue); + queue.isQueued = true; + } else { + queue.isQueued = false; + } + // note: we cannot drain entries from the queue while holding this lock, as it will cause deadlock + // the queue addition holds the per-queue lock first then acquires the lock we have now, but if we + // try to poll now we don't hold the per queue lock but we do hold the global lock... + } + + // parse tasks as long as we are allowed + final long start = System.nanoTime(); + final long deadline = start + pool.queueMaxHoldTime; + do { + try { + if (this.halted) { + break; + } + if (!queue.executeTask()) { + // no more tasks, try next queue + break; + } + ret = true; + } catch (final ThreadDeath death) { + throw death; // goodbye world... + } catch (final Throwable throwable) { + LOGGER.error("Exception thrown from thread '" + this.getName() + "' in queue '" + queue.toString() + "'", throwable); + } + } while (!this.isAlertedHighPriority() && System.nanoTime() <= deadline); + + synchronized (queues) { + // decrement executors, we are no longer executing + if (queue.isQueued) { + queues.remove(queue); + queue.isQueued = false; + } + if (--queue.concurrentExecutors == 0 && queue.scheduledPriority == null) { + // reset scheduling id once the queue is empty again + // this will ensure empty queues are not prioritised suddenly over active queues once tasks are + // queued + queue.schedulingId = 0L; + } + + // ensure the executor is queued for execution again + if (!queue.isHalted && queue.scheduledPriority != null) { // make sure it actually has tasks + queues.add(queue); + queue.isQueued = true; + } + } + } + + return ret; + } + } + + public interface PrioritisedPoolExecutor extends PrioritisedExecutor { + + /** + * Removes this queue from the thread pool without shutting the queue down or waiting for queued tasks to be executed + */ + public void halt(); + + /** + * Returns whether this executor is scheduled to run tasks or is running tasks, otherwise it returns whether + * this queue is not halted and not shutdown. + */ + public boolean isActive(); + } + + protected static final class PrioritisedPoolExecutorImpl extends PrioritisedThreadedTaskQueue implements PrioritisedPoolExecutor { + + protected final PrioritisedThreadPool pool; + protected final long[] priorityCounts = new long[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; + protected long schedulingId; + protected int concurrentExecutors; + protected Priority scheduledPriority; + + protected final String name; + protected final int maximumExecutors; + protected final int minimumExecutors; + protected boolean isQueued; + + public PrioritisedPoolExecutorImpl(final PrioritisedThreadPool pool, final String name, final int maximumExecutors, final int minimumExecutors) { + this.pool = pool; + this.name = name; + this.maximumExecutors = maximumExecutors; + this.minimumExecutors = minimumExecutors; + } + + public static Comparator comparator() { + return (final PrioritisedPoolExecutorImpl p1, final PrioritisedPoolExecutorImpl p2) -> { + if (p1 == p2) { + return 0; + } + + final int belowMin1 = p1.minimumExecutors - p1.concurrentExecutors; + final int belowMin2 = p2.minimumExecutors - p2.concurrentExecutors; + + // test minimum executors + if (belowMin1 > 0 || belowMin2 > 0) { + // want the largest belowMin to be first + final int minCompare = Integer.compare(belowMin2, belowMin1); + + if (minCompare != 0) { + return minCompare; + } + } + + // prefer higher priority + final int priorityCompare = p1.scheduledPriority.ordinal() - p2.scheduledPriority.ordinal(); + if (priorityCompare != 0) { + return priorityCompare; + } + + // try to spread out the executors so that each can have threads executing + final int executorCompare = p1.concurrentExecutors - p2.concurrentExecutors; + if (executorCompare != 0) { + return executorCompare; + } + + // if all else fails here we just choose whichever executor was queued first + return Long.compare(p1.schedulingId, p2.schedulingId); + }; + } + + private boolean isHalted; + + @Override + public void halt() { + final PrioritisedThreadPool pool = this.pool; + final TreeSet queues = pool.queues; + synchronized (queues) { + if (this.isHalted) { + return; + } + this.isHalted = true; + if (this.isQueued) { + queues.remove(this); + this.isQueued = false; + } + } + synchronized (pool.nonShutdownQueues) { + pool.nonShutdownQueues.remove(this); + } + synchronized (pool.activeQueues) { + pool.activeQueues.remove(this); + } + } + + @Override + public boolean isActive() { + final PrioritisedThreadPool pool = this.pool; + final TreeSet queues = pool.queues; + + synchronized (queues) { + if (this.concurrentExecutors != 0) { + return true; + } + synchronized (pool.activeQueues) { + if (pool.activeQueues.contains(this)) { + return true; + } + } + } + + return false; + } + + private long totalQueuedTasks = 0L; + + @Override + protected void priorityChange(final PrioritisedThreadedTaskQueue.PrioritisedTask task, final Priority from, final Priority to) { + // Note: The superclass' queue lock is ALWAYS held when inside this method. So we do NOT need to do any additional synchronisation + // for accessing this queue's state. + final long[] priorityCounts = this.priorityCounts; + final boolean shutdown = this.isShutdown(); + + if (from == null && to == Priority.COMPLETING) { + throw new IllegalStateException("Cannot complete task without queueing it first"); + } + + // we should only notify for queueing of tasks, not changing priorities + final boolean shouldNotifyTasks = from == null; + + final Priority scheduledPriority = this.scheduledPriority; + if (from != null) { + --priorityCounts[from.priority]; + } + if (to != Priority.COMPLETING) { + ++priorityCounts[to.priority]; + } + final long totalQueuedTasks; + if (to == Priority.COMPLETING) { + totalQueuedTasks = --this.totalQueuedTasks; + } else if (from == null) { + totalQueuedTasks = ++this.totalQueuedTasks; + } else { + totalQueuedTasks = this.totalQueuedTasks; + } + + // find new highest priority + int highest = Math.min(to == Priority.COMPLETING ? Priority.IDLE.priority : to.priority, scheduledPriority == null ? Priority.IDLE.priority : scheduledPriority.priority); + int lowestPriority = priorityCounts.length; // exclusive + for (;highest < lowestPriority; ++highest) { + final long count = priorityCounts[highest]; + if (count < 0) { + throw new IllegalStateException("Priority " + highest + " has " + count + " scheduled tasks"); + } + + if (count != 0) { + break; + } + } + + final Priority newPriority; + if (highest == lowestPriority) { + // no tasks left + newPriority = null; + } else if (shutdown) { + // whichever is lower, the actual greatest priority or simply HIGHEST + // this is so shutdown automatically gets priority + newPriority = Priority.getPriority(Math.min(highest, Priority.HIGHEST.priority)); + } else { + newPriority = Priority.getPriority(highest); + } + + final int executorsWanted; + boolean shouldNotifyHighPriority = false; + + final PrioritisedThreadPool pool = this.pool; + final TreeSet queues = pool.queues; + + synchronized (queues) { + if (!this.isQueued) { + // see if we need to be queued + if (newPriority != null) { + if (this.schedulingId == 0L) { + this.schedulingId = ++pool.schedulingIdGenerator; + } + this.scheduledPriority = newPriority; // must be updated before queue add + if (!this.isHalted && this.concurrentExecutors < this.maximumExecutors) { + shouldNotifyHighPriority = newPriority.isHigherOrEqualPriority(Priority.HIGH); + queues.add(this); + this.isQueued = true; + } + } else { + // do not queue + this.scheduledPriority = null; + } + } else { + // see if we need to NOT be queued + if (newPriority == null) { + queues.remove(this); + this.scheduledPriority = null; + this.isQueued = false; + } else if (scheduledPriority != newPriority) { + // if our priority changed, we need to update it - which means removing and re-adding into the queue + queues.remove(this); + // only now can we update scheduledPriority, since we are no longer in queue + this.scheduledPriority = newPriority; + queues.add(this); + shouldNotifyHighPriority = (scheduledPriority == null || scheduledPriority.isLowerPriority(Priority.HIGH)) && newPriority.isHigherOrEqualPriority(Priority.HIGH); + } + } + + if (this.isQueued) { + executorsWanted = Math.min(this.maximumExecutors - this.concurrentExecutors, (int)totalQueuedTasks); + } else { + executorsWanted = 0; + } + } + + if (newPriority == null && shutdown) { + synchronized (pool.activeQueues) { + pool.activeQueues.remove(this); + } + } + + // Wake up the number of executors we want + if (executorsWanted > 0 || (shouldNotifyTasks | shouldNotifyHighPriority)) { + int notified = 0; + for (final PrioritisedThread thread : pool.threads) { + if ((shouldNotifyHighPriority ? thread.alertHighPriorityExecutor() : thread.notifyTasks()) + && (++notified >= executorsWanted)) { + break; + } + } + } + } + + @Override + public boolean shutdown() { + final boolean ret = super.shutdown(); + if (!ret) { + return ret; + } + + final PrioritisedThreadPool pool = this.pool; + + // remove from active queues + synchronized (pool.nonShutdownQueues) { + pool.nonShutdownQueues.remove(this); + } + + final TreeSet queues = pool.queues; + + // try and shift around our priority + synchronized (queues) { + if (this.scheduledPriority == null) { + // no tasks are queued, ensure we aren't in activeQueues + synchronized (pool.activeQueues) { + pool.activeQueues.remove(this); + } + + return ret; + } + + // try to set scheduled priority to HIGHEST so it drains faster + + if (this.scheduledPriority.isHigherOrEqualPriority(Priority.HIGHEST)) { + // already at target priority (highest or above) + return ret; + } + + // shift priority to HIGHEST + + if (this.isQueued) { + queues.remove(this); + this.scheduledPriority = Priority.HIGHEST; + queues.add(this); + } else { + this.scheduledPriority = Priority.HIGHEST; + } + } + + return ret; + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java new file mode 100644 index 0000000..b71404b --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java @@ -0,0 +1,378 @@ +package ca.spottedleaf.concurrentutil.executor.standard; + +import java.util.ArrayDeque; +import java.util.concurrent.atomic.AtomicLong; + +public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor { + + protected final ArrayDeque[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; { + for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) { + this.queues[i] = new ArrayDeque<>(); + } + } + + // Use AtomicLong to separate from the queue field, we don't want false sharing here. + protected final AtomicLong totalScheduledTasks = new AtomicLong(); + protected final AtomicLong totalCompletedTasks = new AtomicLong(); + + // this is here to prevent failures to queue stalling flush() calls (as the schedule calls would increment totalScheduledTasks without this check) + protected volatile boolean hasShutdown; + + protected long taskIdGenerator = 0; + + @Override + public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority) throws IllegalStateException, IllegalArgumentException { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Priority " + priority + " is invalid"); + } + if (task == null) { + throw new NullPointerException("Task cannot be null"); + } + + if (this.hasShutdown) { + // prevent us from stalling flush() calls by incrementing scheduled tasks when we really didn't schedule something + throw new IllegalStateException("Queue has shutdown"); + } + + final PrioritisedTask ret; + + synchronized (this.queues) { + if (this.hasShutdown) { + throw new IllegalStateException("Queue has shutdown"); + } + this.getAndAddTotalScheduledTasksVolatile(1L); + + ret = new PrioritisedTask(this.taskIdGenerator++, task, priority, this); + + this.queues[ret.priority.priority].add(ret); + + // call priority change callback (note: only after we successfully queue!) + this.priorityChange(ret, null, priority); + } + + return ret; + } + + @Override + public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Priority " + priority + " is invalid"); + } + if (task == null) { + throw new NullPointerException("Task cannot be null"); + } + + return new PrioritisedTask(task, priority, this); + } + + @Override + public long getTotalTasksScheduled() { + return this.totalScheduledTasks.get(); + } + + @Override + public long getTotalTasksExecuted() { + return this.totalCompletedTasks.get(); + } + + // callback method for subclasses to override + // from is null when a task is immediately created + protected void priorityChange(final PrioritisedTask task, final Priority from, final Priority to) {} + + /** + * Polls the highest priority task currently available. {@code null} if none. This will mark the + * returned task as completed. + */ + protected PrioritisedTask poll() { + return this.poll(Priority.IDLE); + } + + protected PrioritisedTask poll(final PrioritisedExecutor.Priority minPriority) { + final ArrayDeque[] queues = this.queues; + synchronized (queues) { + final int max = minPriority.priority; + for (int i = 0; i <= max; ++i) { + final ArrayDeque queue = queues[i]; + PrioritisedTask task; + while ((task = queue.pollFirst()) != null) { + if (task.trySetCompleting(i)) { + return task; + } + } + } + } + + return null; + } + + /** + * Polls and executes the highest priority task currently available. Exceptions thrown during task execution will + * be rethrown. + * @return {@code true} if a task was executed, {@code false} otherwise. + */ + @Override + public boolean executeTask() { + final PrioritisedTask task = this.poll(); + + if (task != null) { + task.executeInternal(); + return true; + } + + return false; + } + + @Override + public boolean shutdown() { + synchronized (this.queues) { + if (this.hasShutdown) { + return false; + } + this.hasShutdown = true; + } + return true; + } + + @Override + public boolean isShutdown() { + return this.hasShutdown; + } + + /* totalScheduledTasks */ + + protected final long getTotalScheduledTasksVolatile() { + return this.totalScheduledTasks.get(); + } + + protected final long getAndAddTotalScheduledTasksVolatile(final long value) { + return this.totalScheduledTasks.getAndAdd(value); + } + + /* totalCompletedTasks */ + + protected final long getTotalCompletedTasksVolatile() { + return this.totalCompletedTasks.get(); + } + + protected final long getAndAddTotalCompletedTasksVolatile(final long value) { + return this.totalCompletedTasks.getAndAdd(value); + } + + protected static final class PrioritisedTask implements PrioritisedExecutor.PrioritisedTask { + protected final PrioritisedThreadedTaskQueue queue; + protected long id; + protected static final long NOT_SCHEDULED_ID = -1L; + + protected Runnable runnable; + protected volatile PrioritisedExecutor.Priority priority; + + protected PrioritisedTask(final long id, final Runnable runnable, final PrioritisedExecutor.Priority priority, final PrioritisedThreadedTaskQueue queue) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + this.priority = priority; + this.runnable = runnable; + this.queue = queue; + this.id = id; + } + + protected PrioritisedTask(final Runnable runnable, final PrioritisedExecutor.Priority priority, final PrioritisedThreadedTaskQueue queue) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + this.priority = priority; + this.runnable = runnable; + this.queue = queue; + this.id = NOT_SCHEDULED_ID; + } + + @Override + public boolean queue() { + if (this.queue.hasShutdown) { + throw new IllegalStateException("Queue has shutdown"); + } + + synchronized (this.queue.queues) { + if (this.queue.hasShutdown) { + throw new IllegalStateException("Queue has shutdown"); + } + + final PrioritisedExecutor.Priority priority = this.priority; + if (priority == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (this.id != NOT_SCHEDULED_ID) { + return false; + } + + this.queue.getAndAddTotalScheduledTasksVolatile(1L); + this.id = this.queue.taskIdGenerator++; + this.queue.queues[priority.priority].add(this); + + this.queue.priorityChange(this, null, priority); + + return true; + } + } + + protected boolean trySetCompleting(final int minPriority) { + final PrioritisedExecutor.Priority oldPriority = this.priority; + if (oldPriority != PrioritisedExecutor.Priority.COMPLETING && oldPriority.isHigherOrEqualPriority(minPriority)) { + this.priority = PrioritisedExecutor.Priority.COMPLETING; + if (this.id != NOT_SCHEDULED_ID) { + this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING); + } + return true; + } + + return false; + } + + @Override + public PrioritisedExecutor.Priority getPriority() { + return this.priority; + } + + @Override + public boolean setPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + synchronized (this.queue.queues) { + final PrioritisedExecutor.Priority curr = this.priority; + + if (curr == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (curr == priority) { + return true; + } + + this.priority = priority; + if (this.id != NOT_SCHEDULED_ID) { + this.queue.queues[priority.priority].add(this); + + // call priority change callback + this.queue.priorityChange(this, curr, priority); + } + } + + return true; + } + + @Override + public boolean raisePriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + synchronized (this.queue.queues) { + final PrioritisedExecutor.Priority curr = this.priority; + + if (curr == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (curr.isHigherOrEqualPriority(priority)) { + return true; + } + + this.priority = priority; + if (this.id != NOT_SCHEDULED_ID) { + this.queue.queues[priority.priority].add(this); + + // call priority change callback + this.queue.priorityChange(this, curr, priority); + } + } + + return true; + } + + @Override + public boolean lowerPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + synchronized (this.queue.queues) { + final PrioritisedExecutor.Priority curr = this.priority; + + if (curr == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (curr.isLowerOrEqualPriority(priority)) { + return true; + } + + this.priority = priority; + if (this.id != NOT_SCHEDULED_ID) { + this.queue.queues[priority.priority].add(this); + + // call priority change callback + this.queue.priorityChange(this, curr, priority); + } + } + + return true; + } + + @Override + public boolean cancel() { + final long id; + synchronized (this.queue.queues) { + final Priority oldPriority = this.priority; + if (oldPriority == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + this.priority = PrioritisedExecutor.Priority.COMPLETING; + // call priority change callback + if ((id = this.id) != NOT_SCHEDULED_ID) { + this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING); + } + } + this.runnable = null; + if (id != NOT_SCHEDULED_ID) { + this.queue.getAndAddTotalCompletedTasksVolatile(1L); + } + return true; + } + + protected void executeInternal() { + try { + final Runnable execute = this.runnable; + this.runnable = null; + execute.run(); + } finally { + if (this.id != NOT_SCHEDULED_ID) { + this.queue.getAndAddTotalCompletedTasksVolatile(1L); + } + } + } + + @Override + public boolean execute() { + synchronized (this.queue.queues) { + final Priority oldPriority = this.priority; + if (oldPriority == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + this.priority = PrioritisedExecutor.Priority.COMPLETING; + // call priority change callback + if (this.id != NOT_SCHEDULED_ID) { + this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING); + } + } + + this.executeInternal(); + return true; + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java new file mode 100644 index 0000000..4fd9a0c --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java @@ -0,0 +1,395 @@ +package ca.spottedleaf.concurrentutil.lock; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import it.unimi.dsi.fastutil.HashCommon; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.LockSupport; + +public final class ReentrantAreaLock { + + public final int coordinateShift; + + // aggressive load factor to reduce contention + private final ConcurrentHashMap nodes = new ConcurrentHashMap<>(128, 0.2f); + + public ReentrantAreaLock(final int coordinateShift) { + this.coordinateShift = coordinateShift; + } + + public boolean isHeldByCurrentThread(final int x, final int z) { + final Thread currThread = Thread.currentThread(); + final int shift = this.coordinateShift; + final int sectionX = x >> shift; + final int sectionZ = z >> shift; + + final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); + final Node node = this.nodes.get(coordinate); + + return node != null && node.thread == currThread; + } + + public boolean isHeldByCurrentThread(final int centerX, final int centerZ, final int radius) { + return this.isHeldByCurrentThread(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius); + } + + public boolean isHeldByCurrentThread(final int fromX, final int fromZ, final int toX, final int toZ) { + if (fromX > toX || fromZ > toZ) { + throw new IllegalArgumentException(); + } + + final Thread currThread = Thread.currentThread(); + final int shift = this.coordinateShift; + final int fromSectionX = fromX >> shift; + final int fromSectionZ = fromZ >> shift; + final int toSectionX = toX >> shift; + final int toSectionZ = toZ >> shift; + + for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { + for (int currX = fromSectionX; currX <= toSectionX; ++currX) { + final Coordinate coordinate = new Coordinate(Coordinate.key(currX, currZ)); + + final Node node = this.nodes.get(coordinate); + + if (node == null || node.thread != currThread) { + return false; + } + } + } + + return true; + } + + public Node tryLock(final int x, final int z) { + return this.tryLock(x, z, x, z); + } + + public Node tryLock(final int centerX, final int centerZ, final int radius) { + return this.tryLock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius); + } + + public Node tryLock(final int fromX, final int fromZ, final int toX, final int toZ) { + if (fromX > toX || fromZ > toZ) { + throw new IllegalArgumentException(); + } + + final Thread currThread = Thread.currentThread(); + final int shift = this.coordinateShift; + final int fromSectionX = fromX >> shift; + final int fromSectionZ = fromZ >> shift; + final int toSectionX = toX >> shift; + final int toSectionZ = toZ >> shift; + + final List areaAffected = new ArrayList<>(); + + final Node ret = new Node(this, areaAffected, currThread); + + boolean failed = false; + + // try to fast acquire area + for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { + for (int currX = fromSectionX; currX <= toSectionX; ++currX) { + final Coordinate coordinate = new Coordinate(Coordinate.key(currX, currZ)); + + final Node prev = this.nodes.putIfAbsent(coordinate, ret); + + if (prev == null) { + areaAffected.add(coordinate); + continue; + } + + if (prev.thread != currThread) { + failed = true; + break; + } + } + } + + if (!failed) { + return ret; + } + + // failed, undo logic + if (!areaAffected.isEmpty()) { + for (int i = 0, len = areaAffected.size(); i < len; ++i) { + final Coordinate key = areaAffected.get(i); + + if (this.nodes.remove(key) != ret) { + throw new IllegalStateException(); + } + } + + areaAffected.clear(); + + // since we inserted, we need to drain waiters + Thread unpark; + while ((unpark = ret.pollOrBlockAdds()) != null) { + LockSupport.unpark(unpark); + } + } + + return null; + } + + public Node lock(final int x, final int z) { + final Thread currThread = Thread.currentThread(); + final int shift = this.coordinateShift; + final int sectionX = x >> shift; + final int sectionZ = z >> shift; + + final List areaAffected = new ArrayList<>(1); + + final Node ret = new Node(this, areaAffected, currThread); + final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); + + for (long failures = 0L;;) { + final Node park; + + // try to fast acquire area + { + final Node prev = this.nodes.putIfAbsent(coordinate, ret); + + if (prev == null) { + areaAffected.add(coordinate); + return ret; + } else if (prev.thread != currThread) { + park = prev; + } else { + // only one node we would want to acquire, and it's owned by this thread already + return ret; + } + } + + ++failures; + + if (failures > 128L && park.add(currThread)) { + LockSupport.park(); + } else { + // high contention, spin wait + if (failures < 128L) { + for (long i = 0; i < failures; ++i) { + Thread.onSpinWait(); + } + failures = failures << 1; + } else if (failures < 1_200L) { + LockSupport.parkNanos(1_000L); + failures = failures + 1L; + } else { // scale 0.1ms (100us) per failure + Thread.yield(); + LockSupport.parkNanos(100_000L * failures); + failures = failures + 1L; + } + } + } + } + + public Node lock(final int centerX, final int centerZ, final int radius) { + return this.lock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius); + } + + public Node lock(final int fromX, final int fromZ, final int toX, final int toZ) { + if (fromX > toX || fromZ > toZ) { + throw new IllegalArgumentException(); + } + + final Thread currThread = Thread.currentThread(); + final int shift = this.coordinateShift; + final int fromSectionX = fromX >> shift; + final int fromSectionZ = fromZ >> shift; + final int toSectionX = toX >> shift; + final int toSectionZ = toZ >> shift; + + if (((fromSectionX ^ toSectionX) | (fromSectionZ ^ toSectionZ)) == 0) { + return this.lock(fromX, fromZ); + } + + final List areaAffected = new ArrayList<>(); + + final Node ret = new Node(this, areaAffected, currThread); + + for (long failures = 0L;;) { + Node park = null; + boolean addedToArea = false; + boolean alreadyOwned = false; + boolean allOwned = true; + + // try to fast acquire area + for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { + for (int currX = fromSectionX; currX <= toSectionX; ++currX) { + final Coordinate coordinate = new Coordinate(Coordinate.key(currX, currZ)); + + final Node prev = this.nodes.putIfAbsent(coordinate, ret); + + if (prev == null) { + addedToArea = true; + allOwned = false; + areaAffected.add(coordinate); + continue; + } + + if (prev.thread != currThread) { + park = prev; + alreadyOwned = true; + break; + } + } + } + + if (park == null) { + if (alreadyOwned && !allOwned) { + throw new IllegalStateException("Improper lock usage: Should never acquire intersecting areas"); + } + return ret; + } + + // failed, undo logic + if (addedToArea) { + for (int i = 0, len = areaAffected.size(); i < len; ++i) { + final Coordinate key = areaAffected.get(i); + + if (this.nodes.remove(key) != ret) { + throw new IllegalStateException(); + } + } + + areaAffected.clear(); + + // since we inserted, we need to drain waiters + Thread unpark; + while ((unpark = ret.pollOrBlockAdds()) != null) { + LockSupport.unpark(unpark); + } + } + + ++failures; + + if (failures > 128L && park.add(currThread)) { + LockSupport.park(park); + } else { + // high contention, spin wait + if (failures < 128L) { + for (long i = 0; i < failures; ++i) { + Thread.onSpinWait(); + } + failures = failures << 1; + } else if (failures < 1_200L) { + LockSupport.parkNanos(1_000L); + failures = failures + 1L; + } else { // scale 0.1ms (100us) per failure + Thread.yield(); + LockSupport.parkNanos(100_000L * failures); + failures = failures + 1L; + } + } + + if (addedToArea) { + // try again, so we need to allow adds so that other threads can properly block on us + ret.allowAdds(); + } + } + } + + public void unlock(final Node node) { + if (node.lock != this) { + throw new IllegalStateException("Unlock target lock mismatch"); + } + + final List areaAffected = node.areaAffected; + + if (areaAffected.isEmpty()) { + // here we are not in the node map, and so do not need to remove from the node map or unblock any waiters + return; + } + + // remove from node map; allowing other threads to lock + for (int i = 0, len = areaAffected.size(); i < len; ++i) { + final Coordinate coordinate = areaAffected.get(i); + if (this.nodes.remove(coordinate) != node) { + throw new IllegalStateException(); + } + } + + Thread unpark; + while ((unpark = node.pollOrBlockAdds()) != null) { + LockSupport.unpark(unpark); + } + } + + public static final class Node extends MultiThreadedQueue { + + private final ReentrantAreaLock lock; + private final List areaAffected; + private final Thread thread; + //private final Throwable WHO_CREATED_MY_ASS = new Throwable(); + + private Node(final ReentrantAreaLock lock, final List areaAffected, final Thread thread) { + this.lock = lock; + this.areaAffected = areaAffected; + this.thread = thread; + } + + @Override + public String toString() { + return "Node{" + + "areaAffected=" + this.areaAffected + + ", thread=" + this.thread + + '}'; + } + } + + private static final class Coordinate implements Comparable { + + public final long key; + + public Coordinate(final long key) { + this.key = key; + } + + public Coordinate(final int x, final int z) { + this.key = key(x, z); + } + + public static long key(final int x, final int z) { + return ((long)z << 32) | (x & 0xFFFFFFFFL); + } + + public static int x(final long key) { + return (int)key; + } + + public static int z(final long key) { + return (int)(key >>> 32); + } + + @Override + public int hashCode() { + return (int)HashCommon.mix(this.key); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof Coordinate other)) { + return false; + } + + return this.key == other.key; + } + + // This class is intended for HashMap/ConcurrentHashMap usage, which do treeify bin nodes if the chain + // is too large. So we should implement compareTo to help. + @Override + public int compareTo(final Coordinate other) { + return Long.compare(this.key, other.key); + } + + @Override + public String toString() { + return "[" + x(this.key) + "," + z(this.key) + "]"; + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/SyncReentrantAreaLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/SyncReentrantAreaLock.java new file mode 100644 index 0000000..64b5803 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/SyncReentrantAreaLock.java @@ -0,0 +1,217 @@ +package ca.spottedleaf.concurrentutil.lock; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import java.util.concurrent.locks.LockSupport; + +// not concurrent, unlike ReentrantAreaLock +// no incorrect lock usage detection (acquiring intersecting areas) +// this class is nothing more than a performance reference for ReentrantAreaLock +public final class SyncReentrantAreaLock { + + private final int coordinateShift; + + // aggressive load factor to reduce contention + private final Long2ReferenceOpenHashMap nodes = new Long2ReferenceOpenHashMap<>(128, 0.2f); + + public SyncReentrantAreaLock(final int coordinateShift) { + this.coordinateShift = coordinateShift; + } + + private static long key(final int x, final int z) { + return ((long)z << 32) | (x & 0xFFFFFFFFL); + } + + public Node lock(final int x, final int z) { + final Thread currThread = Thread.currentThread(); + final int shift = this.coordinateShift; + final int sectionX = x >> shift; + final int sectionZ = z >> shift; + + final LongArrayList areaAffected = new LongArrayList(); + + final Node ret = new Node(this, areaAffected, currThread); + + final long coordinate = key(sectionX, sectionZ); + + for (long failures = 0L;;) { + final Node park; + + synchronized (this) { + // try to fast acquire area + final Node prev = this.nodes.putIfAbsent(coordinate, ret); + + if (prev == null) { + areaAffected.add(coordinate); + return ret; + } else if (prev.thread != currThread) { + park = prev; + } else { + // only one node we would want to acquire, and it's owned by this thread already + return ret; + } + } + + ++failures; + + if (failures > 128L && park.add(currThread)) { + LockSupport.park(); + } else { + // high contention, spin wait + if (failures < 128L) { + for (long i = 0; i < failures; ++i) { + Thread.onSpinWait(); + } + failures = failures << 1; + } else if (failures < 1_200L) { + LockSupport.parkNanos(1_000L); + failures = failures + 1L; + } else { // scale 0.1ms (100us) per failure + Thread.yield(); + LockSupport.parkNanos(100_000L * failures); + failures = failures + 1L; + } + } + } + } + + public Node lock(final int centerX, final int centerZ, final int radius) { + return this.lock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius); + } + + public Node lock(final int fromX, final int fromZ, final int toX, final int toZ) { + if (fromX > toX || fromZ > toZ) { + throw new IllegalArgumentException(); + } + + final Thread currThread = Thread.currentThread(); + final int shift = this.coordinateShift; + final int fromSectionX = fromX >> shift; + final int fromSectionZ = fromZ >> shift; + final int toSectionX = toX >> shift; + final int toSectionZ = toZ >> shift; + + final LongArrayList areaAffected = new LongArrayList(); + + final Node ret = new Node(this, areaAffected, currThread); + + for (long failures = 0L;;) { + Node park = null; + boolean addedToArea = false; + + synchronized (this) { + // try to fast acquire area + for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { + for (int currX = fromSectionX; currX <= toSectionX; ++currX) { + final long coordinate = key(currX, currZ); + + final Node prev = this.nodes.putIfAbsent(coordinate, ret); + + if (prev == null) { + addedToArea = true; + areaAffected.add(coordinate); + continue; + } + + if (prev.thread != currThread) { + park = prev; + break; + } + } + } + + if (park == null) { + return ret; + } + + // failed, undo logic + if (!areaAffected.isEmpty()) { + for (int i = 0, len = areaAffected.size(); i < len; ++i) { + final long key = areaAffected.getLong(i); + + if (!this.nodes.remove(key, ret)) { + throw new IllegalStateException(); + } + } + } + } + + if (addedToArea) { + areaAffected.clear(); + // since we inserted, we need to drain waiters + Thread unpark; + while ((unpark = ret.pollOrBlockAdds()) != null) { + LockSupport.unpark(unpark); + } + } + + ++failures; + + if (failures > 128L && park.add(currThread)) { + LockSupport.park(); + } else { + // high contention, spin wait + if (failures < 128L) { + for (long i = 0; i < failures; ++i) { + Thread.onSpinWait(); + } + failures = failures << 1; + } else if (failures < 1_200L) { + LockSupport.parkNanos(1_000L); + failures = failures + 1L; + } else { // scale 0.1ms (100us) per failure + Thread.yield(); + LockSupport.parkNanos(100_000L * failures); + failures = failures + 1L; + } + } + + if (addedToArea) { + // try again, so we need to allow adds so that other threads can properly block on us + ret.allowAdds(); + } + } + } + + public void unlock(final Node node) { + if (node.lock != this) { + throw new IllegalStateException("Unlock target lock mismatch"); + } + + final LongArrayList areaAffected = node.areaAffected; + + if (areaAffected.isEmpty()) { + // here we are not in the node map, and so do not need to remove from the node map or unblock any waiters + return; + } + + // remove from node map; allowing other threads to lock + synchronized (this) { + for (int i = 0, len = areaAffected.size(); i < len; ++i) { + final long coordinate = areaAffected.getLong(i); + if (!this.nodes.remove(coordinate, node)) { + throw new IllegalStateException(); + } + } + } + + Thread unpark; + while ((unpark = node.pollOrBlockAdds()) != null) { + LockSupport.unpark(unpark); + } + } + + public static final class Node extends MultiThreadedQueue { + + private final SyncReentrantAreaLock lock; + private final LongArrayList areaAffected; + private final Thread thread; + + private Node(final SyncReentrantAreaLock lock, final LongArrayList areaAffected, final Thread thread) { + this.lock = lock; + this.areaAffected = areaAffected; + this.thread = thread; + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java new file mode 100644 index 0000000..6d4f7ae --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java @@ -0,0 +1,1673 @@ +package ca.spottedleaf.concurrentutil.map; + +import ca.spottedleaf.concurrentutil.util.ArrayUtil; +import ca.spottedleaf.concurrentutil.util.CollectionUtil; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.IntegerUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import java.lang.invoke.VarHandle; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.Predicate; + +/** + *

+ * Note: Not really tested, use at your own risk. + *

+ * This map is safe for reading from multiple threads, however it is only safe to write from a single thread. + * {@code null} keys or values are not permitted. Writes to values in this map are guaranteed to be ordered by release semantics, + * however immediate visibility to other threads is not guaranteed. However, writes are guaranteed to be made visible eventually. + * Reads are ordered by acquire semantics. + *

+ * Iterators cannot be modified concurrently, and its backing map cannot be modified concurrently. There is no + * fast-fail attempt made by iterators, thus modifying the iterator's backing map while iterating will have undefined + * behaviour. + *

+ *

+ * Subclasses should override {@link #clone()} to return correct instances of this class. + *

+ * @param {@inheritDoc} + * @param {@inheritDoc} + */ +public class SWMRHashTable implements Map, Iterable> { + + protected int size; + + protected TableEntry[] table; + + protected final float loadFactor; + + protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRHashTable.class, "size", int.class); + protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRHashTable.class, "table", TableEntry[].class); + + /* size */ + + protected final int getSizePlain() { + return (int)SIZE_HANDLE.get(this); + } + + protected final int getSizeOpaque() { + return (int)SIZE_HANDLE.getOpaque(this); + } + + protected final int getSizeAcquire() { + return (int)SIZE_HANDLE.getAcquire(this); + } + + protected final void setSizePlain(final int value) { + SIZE_HANDLE.set(this, value); + } + + protected final void setSizeOpaque(final int value) { + SIZE_HANDLE.setOpaque(this, value); + } + + protected final void setSizeRelease(final int value) { + SIZE_HANDLE.setRelease(this, value); + } + + /* table */ + + protected final TableEntry[] getTablePlain() { + //noinspection unchecked + return (TableEntry[])TABLE_HANDLE.get(this); + } + + protected final TableEntry[] getTableAcquire() { + //noinspection unchecked + return (TableEntry[])TABLE_HANDLE.getAcquire(this); + } + + protected final void setTablePlain(final TableEntry[] table) { + TABLE_HANDLE.set(this, table); + } + + protected final void setTableRelease(final TableEntry[] table) { + TABLE_HANDLE.setRelease(this, table); + } + + protected static final int DEFAULT_CAPACITY = 16; + protected static final float DEFAULT_LOAD_FACTOR = 0.75f; + protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1; + + /** + * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}. + */ + public SWMRHashTable() { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs this map with the specified capacity and load factor of {@code 0.75f}. + * @param capacity specified initial capacity, > 0 + */ + public SWMRHashTable(final int capacity) { + this(capacity, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs this map with the specified capacity and load factor. + * @param capacity specified capacity, > 0 + * @param loadFactor specified load factor, > 0 && finite + */ + public SWMRHashTable(final int capacity, final float loadFactor) { + final int tableSize = getCapacityFor(capacity); + + if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) { + throw new IllegalArgumentException("Invalid load factor: " + loadFactor); + } + + //noinspection unchecked + final TableEntry[] table = new TableEntry[tableSize]; + this.setTablePlain(table); + + if (tableSize == MAXIMUM_CAPACITY) { + this.threshold = -1; + } else { + this.threshold = getTargetCapacity(tableSize, loadFactor); + } + + this.loadFactor = loadFactor; + } + + /** + * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and + * with a load factor of {@code 0.75f}. + * All of the specified map's entries are copied into this map. + * @param other The specified map. + */ + public SWMRHashTable(final Map other) { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other); + } + + /** + * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and + * with a load factor of {@code 0.75f}. + * All of the specified map's entries are copied into this map. + * @param capacity specified capacity, > 0 + * @param other The specified map. + */ + public SWMRHashTable(final int capacity, final Map other) { + this(capacity, DEFAULT_LOAD_FACTOR, other); + } + + /** + * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and + * with the specified load factor. + * All of the specified map's entries are copied into this map. + * @param capacity specified capacity, > 0 + * @param loadFactor specified load factor, > 0 && finite + * @param other The specified map. + */ + public SWMRHashTable(final int capacity, final float loadFactor, final Map other) { + this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor); + this.putAll(other); + } + + public final float getLoadFactor() { + return this.loadFactor; + } + + protected static int getCapacityFor(final int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("Invalid capacity: " + capacity); + } + if (capacity >= MAXIMUM_CAPACITY) { + return MAXIMUM_CAPACITY; + } + return IntegerUtil.roundCeilLog2(capacity); + } + + /** Callers must still use acquire when reading the value of the entry. */ + protected final TableEntry getEntryForOpaque(final K key) { + final int hash = SWMRHashTable.getHash(key); + final TableEntry[] table = this.getTableAcquire(); + + for (TableEntry curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) { + if (hash == curr.hash && (key == curr.key || curr.key.equals(key))) { + return curr; + } + } + + return null; + } + + protected final TableEntry getEntryForPlain(final K key) { + final int hash = SWMRHashTable.getHash(key); + final TableEntry[] table = this.getTablePlain(); + + for (TableEntry curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) { + if (hash == curr.hash && (key == curr.key || curr.key.equals(key))) { + return curr; + } + } + + return null; + } + + /* MT-Safe */ + + /** must be deterministic given a key */ + private static int getHash(final Object key) { + int hash = key == null ? 0 : key.hashCode(); + // inlined IntegerUtil#hash0 + hash *= 0x36935555; + hash ^= hash >>> 16; + return hash; + } + + static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash + static final int spread(int h) { + return (h ^ (h >>> 16)) & HASH_BITS; + } + + // rets -1 if capacity*loadFactor is too large + protected static int getTargetCapacity(final int capacity, final float loadFactor) { + final double ret = (double)capacity * (double)loadFactor; + if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) { + return -1; + } + + return (int)ret; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + /* Make no attempt to deal with concurrent modifications */ + if (!(obj instanceof Map)) { + return false; + } + final Map other = (Map)obj; + + if (this.size() != other.size()) { + return false; + } + + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + final Object otherValue = other.get(curr.key); + if (otherValue == null || (value != otherValue && value.equals(otherValue))) { + return false; + } + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + /* Make no attempt to deal with concurrent modifications */ + int hash = 0; + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + hash += curr.hashCode(); + } + } + + return hash; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(64); + builder.append("SingleWriterMultiReaderHashMap:{"); + + this.forEach((final K key, final V value) -> { + builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}"); + }); + + return builder.append('}').toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public SWMRHashTable clone() { + return new SWMRHashTable<>(this.getTableAcquire().length, this.loadFactor, this); + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator> iterator() { + return new EntryIterator<>(this.getTableAcquire(), this); + } + + /** + * {@inheritDoc} + */ + @Override + public void forEach(final Consumer> action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void forEach(final BiConsumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + action.accept(curr.key, value); + } + } + } + + /** + * Provides the specified consumer with all keys contained within this map. + * @param action The specified consumer. + */ + public void forEachKey(final Consumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr.key); + } + } + } + + /** + * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}. + * @param action The specified consumer. + */ + public void forEachValue(final Consumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + action.accept(value); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public V get(final Object key) { + Validate.notNull(key, "Null key"); + + //noinspection unchecked + final TableEntry entry = this.getEntryForOpaque((K)key); + return entry == null ? null : entry.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean containsKey(final Object key) { + Validate.notNull(key, "Null key"); + + // note: we need to use getValueAcquire, so that the reads from this map are ordered by acquire semantics + return this.get(key) != null; + } + + /** + * Returns {@code true} if this map contains an entry with the specified key and value at some point during this call. + * @param key The specified key. + * @param value The specified value. + * @return {@code true} if this map contains an entry with the specified key and value. + */ + public boolean contains(final Object key, final Object value) { + Validate.notNull(key, "Null key"); + + //noinspection unchecked + final TableEntry entry = this.getEntryForOpaque((K)key); + + if (entry == null) { + return false; + } + + final V entryVal = entry.getValueAcquire(); + return entryVal == value || entryVal.equals(value); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean containsValue(final Object value) { + Validate.notNull(value, "Null value"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V currVal = curr.getValueAcquire(); + if (currVal == value || currVal.equals(value)) { + return true; + } + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public V getOrDefault(final Object key, final V defaultValue) { + Validate.notNull(key, "Null key"); + + //noinspection unchecked + final TableEntry entry = this.getEntryForOpaque((K)key); + + return entry == null ? defaultValue : entry.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + @Override + public int size() { + return this.getSizeAcquire(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isEmpty() { + return this.getSizeAcquire() == 0; + } + + protected Set keyset; + protected Collection values; + protected Set> entrySet; + + @Override + public Set keySet() { + return this.keyset == null ? this.keyset = new KeySet<>(this) : this.keyset; + } + + @Override + public Collection values() { + return this.values == null ? this.values = new ValueCollection<>(this) : this.values; + } + + @Override + public Set> entrySet() { + return this.entrySet == null ? this.entrySet = new EntrySet<>(this) : this.entrySet; + } + + /* Non-MT-Safe */ + + protected int threshold; + + protected final void checkResize(final int minCapacity) { + if (minCapacity <= this.threshold || this.threshold < 0) { + return; + } + + final TableEntry[] table = this.getTablePlain(); + int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity); + if (newCapacity < 0) { + newCapacity = MAXIMUM_CAPACITY; + } + if (newCapacity <= table.length) { + if (newCapacity == MAXIMUM_CAPACITY) { + return; + } + newCapacity = table.length << 1; + } + + //noinspection unchecked + final TableEntry[] newTable = new TableEntry[newCapacity]; + final int indexMask = newCapacity - 1; + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry entry = table[i]; entry != null; entry = entry.getNextPlain()) { + final int hash = entry.hash; + final int index = hash & indexMask; + + /* we need to create a new entry since there could be reading threads */ + final TableEntry insert = new TableEntry<>(hash, entry.key, entry.getValuePlain()); + + final TableEntry prev = newTable[index]; + + newTable[index] = insert; + insert.setNextPlain(prev); + } + } + + if (newCapacity == MAXIMUM_CAPACITY) { + this.threshold = -1; /* No more resizing */ + } else { + this.threshold = getTargetCapacity(newCapacity, this.loadFactor); + } + this.setTableRelease(newTable); /* use release to publish entries in table */ + } + + protected final int addToSize(final int num) { + final int newSize = this.getSizePlain() + num; + + this.setSizeOpaque(newSize); + this.checkResize(newSize); + + return newSize; + } + + protected final int removeFromSize(final int num) { + final int newSize = this.getSizePlain() - num; + + this.setSizeOpaque(newSize); + + return newSize; + } + + /* Cannot be used to perform downsizing */ + protected final int removeFromSizePlain(final int num) { + final int newSize = this.getSizePlain() - num; + + this.setSizePlain(newSize); + + return newSize; + } + + protected final V put(final K key, final V value, final boolean onlyIfAbsent) { + final TableEntry[] table = this.getTablePlain(); + final int hash = SWMRHashTable.getHash(key); + final int index = hash & (table.length - 1); + + final TableEntry head = table[index]; + if (head == null) { + final TableEntry insert = new TableEntry<>(hash, key, value); + ArrayUtil.setRelease(table, index, insert); + this.addToSize(1); + return null; + } + + for (TableEntry curr = head;;) { + if (curr.hash == hash && (key == curr.key || curr.key.equals(key))) { + if (onlyIfAbsent) { + return curr.getValuePlain(); + } + + final V currVal = curr.getValuePlain(); + curr.setValueRelease(value); + return currVal; + } + + final TableEntry next = curr.getNextPlain(); + if (next != null) { + curr = next; + continue; + } + + final TableEntry insert = new TableEntry<>(hash, key, value); + + curr.setNextRelease(insert); + this.addToSize(1); + return null; + } + } + + /** + * Removes a key-value pair from this map if the specified predicate returns true. The specified predicate is + * tested with every entry in this map. Returns the number of key-value pairs removed. + * @param predicate The predicate to test key-value pairs against. + * @return The total number of key-value pairs removed from this map. + */ + public int removeIf(final BiPredicate predicate) { + Validate.notNull(predicate, "Null predicate"); + + int removed = 0; + + final TableEntry[] table = this.getTablePlain(); + + bin_iteration_loop: + for (int i = 0, len = table.length; i < len; ++i) { + TableEntry curr = table[i]; + if (curr == null) { + continue; + } + + /* Handle bin nodes first */ + while (predicate.test(curr.key, curr.getValuePlain())) { + ++removed; + this.removeFromSizePlain(1); /* required in case predicate throws an exception */ + + ArrayUtil.setRelease(table, i, curr = curr.getNextPlain()); + + if (curr == null) { + continue bin_iteration_loop; + } + } + + TableEntry prev; + + /* curr at this point is the bin node */ + + for (prev = curr, curr = curr.getNextPlain(); curr != null;) { + /* If we want to remove, then we should hold prev, as it will be a valid entry to link on */ + if (predicate.test(curr.key, curr.getValuePlain())) { + ++removed; + this.removeFromSizePlain(1); /* required in case predicate throws an exception */ + + prev.setNextRelease(curr = curr.getNextPlain()); + } else { + prev = curr; + curr = curr.getNextPlain(); + } + } + } + + return removed; + } + + /** + * Removes a key-value pair from this map if the specified predicate returns true. The specified predicate is + * tested with every entry in this map. Returns the number of key-value pairs removed. + * @param predicate The predicate to test key-value pairs against. + * @return The total number of key-value pairs removed from this map. + */ + public int removeEntryIf(final Predicate> predicate) { + Validate.notNull(predicate, "Null predicate"); + + int removed = 0; + + final TableEntry[] table = this.getTablePlain(); + + bin_iteration_loop: + for (int i = 0, len = table.length; i < len; ++i) { + TableEntry curr = table[i]; + if (curr == null) { + continue; + } + + /* Handle bin nodes first */ + while (predicate.test(curr)) { + ++removed; + this.removeFromSizePlain(1); /* required in case predicate throws an exception */ + + ArrayUtil.setRelease(table, i, curr = curr.getNextPlain()); + + if (curr == null) { + continue bin_iteration_loop; + } + } + + TableEntry prev; + + /* curr at this point is the bin node */ + + for (prev = curr, curr = curr.getNextPlain(); curr != null;) { + /* If we want to remove, then we should hold prev, as it will be a valid entry to link on */ + if (predicate.test(curr)) { + ++removed; + this.removeFromSizePlain(1); /* required in case predicate throws an exception */ + + prev.setNextRelease(curr = curr.getNextPlain()); + } else { + prev = curr; + curr = curr.getNextPlain(); + } + } + } + + return removed; + } + + /** + * {@inheritDoc} + */ + @Override + public V put(final K key, final V value) { + Validate.notNull(key, "Null key"); + Validate.notNull(value, "Null value"); + + return this.put(key, value, false); + } + + /** + * {@inheritDoc} + */ + @Override + public V putIfAbsent(final K key, final V value) { + Validate.notNull(key, "Null key"); + Validate.notNull(value, "Null value"); + + return this.put(key, value, true); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean remove(final Object key, final Object value) { + Validate.notNull(key, "Null key"); + Validate.notNull(value, "Null value"); + + final TableEntry[] table = this.getTablePlain(); + final int hash = SWMRHashTable.getHash(key); + final int index = hash & (table.length - 1); + + final TableEntry head = table[index]; + if (head == null) { + return false; + } + + if (head.hash == hash && (head.key == key || head.key.equals(key))) { + final V currVal = head.getValuePlain(); + + if (currVal != value && !currVal.equals(value)) { + return false; + } + + ArrayUtil.setRelease(table, index, head.getNextPlain()); + this.removeFromSize(1); + + return true; + } + + for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { + if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) { + final V currVal = curr.getValuePlain(); + + if (currVal != value && !currVal.equals(value)) { + return false; + } + + prev.setNextRelease(curr.getNextPlain()); + this.removeFromSize(1); + + return true; + } + } + + return false; + } + + protected final V remove(final Object key, final int hash) { + final TableEntry[] table = this.getTablePlain(); + final int index = (table.length - 1) & hash; + + final TableEntry head = table[index]; + if (head == null) { + return null; + } + + if (hash == head.hash && (head.key == key || head.key.equals(key))) { + ArrayUtil.setRelease(table, index, head.getNextPlain()); + this.removeFromSize(1); + + return head.getValuePlain(); + } + + for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { + if (curr.hash == hash && (key == curr.key || curr.key.equals(key))) { + prev.setNextRelease(curr.getNextPlain()); + this.removeFromSize(1); + + return curr.getValuePlain(); + } + } + + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public V remove(final Object key) { + Validate.notNull(key, "Null key"); + + return this.remove(key, SWMRHashTable.getHash(key)); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean replace(final K key, final V oldValue, final V newValue) { + Validate.notNull(key, "Null key"); + Validate.notNull(oldValue, "Null oldValue"); + Validate.notNull(newValue, "Null newValue"); + + final TableEntry entry = this.getEntryForPlain(key); + if (entry == null) { + return false; + } + + final V currValue = entry.getValuePlain(); + if (currValue == oldValue || currValue.equals(oldValue)) { + entry.setValueRelease(newValue); + return true; + } + + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public V replace(final K key, final V value) { + Validate.notNull(key, "Null key"); + Validate.notNull(value, "Null value"); + + final TableEntry entry = this.getEntryForPlain(key); + if (entry == null) { + return null; + } + + final V prev = entry.getValuePlain(); + entry.setValueRelease(value); + return prev; + } + + /** + * {@inheritDoc} + */ + @Override + public void replaceAll(final BiFunction function) { + Validate.notNull(function, "Null function"); + + final TableEntry[] table = this.getTablePlain(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = table[i]; curr != null; curr = curr.getNextPlain()) { + final V value = curr.getValuePlain(); + + final V newValue = function.apply(curr.key, value); + if (newValue == null) { + throw new NullPointerException(); + } + + curr.setValueRelease(newValue); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void putAll(final Map map) { + Validate.notNull(map, "Null map"); + + final int size = map.size(); + this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */ + map.forEach(this::put); + } + + /** + * {@inheritDoc} + *

+ * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself + * is release ordered, that is, after the clear operation is performed a release fence is performed. + *

+ */ + @Override + public void clear() { + Arrays.fill(this.getTablePlain(), null); + this.setSizeRelease(0); + } + + /** + * {@inheritDoc} + */ + @Override + public V compute(final K key, final BiFunction remappingFunction) { + Validate.notNull(key, "Null key"); + Validate.notNull(remappingFunction, "Null remappingFunction"); + + final int hash = SWMRHashTable.getHash(key); + final TableEntry[] table = this.getTablePlain(); + final int index = hash & (table.length - 1); + + for (TableEntry curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) { + if (curr == null) { + final V newVal = remappingFunction.apply(key ,null); + + if (newVal == null) { + return null; + } + + final TableEntry insert = new TableEntry<>(hash, key, newVal); + if (prev == null) { + ArrayUtil.setRelease(table, index, insert); + } else { + prev.setNextRelease(insert); + } + + this.addToSize(1); + + return newVal; + } + + if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) { + final V newVal = remappingFunction.apply(key, curr.getValuePlain()); + + if (newVal != null) { + curr.setValueRelease(newVal); + return newVal; + } + + if (prev == null) { + ArrayUtil.setRelease(table, index, curr.getNextPlain()); + } else { + prev.setNextRelease(curr.getNextPlain()); + } + + this.removeFromSize(1); + + return null; + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public V computeIfPresent(final K key, final BiFunction remappingFunction) { + Validate.notNull(key, "Null key"); + Validate.notNull(remappingFunction, "Null remappingFunction"); + + final int hash = SWMRHashTable.getHash(key); + final TableEntry[] table = this.getTablePlain(); + final int index = hash & (table.length - 1); + + for (TableEntry curr = table[index], prev = null; curr != null; prev = curr, curr = curr.getNextPlain()) { + if (curr.hash != hash || (curr.key != key && !curr.key.equals(key))) { + continue; + } + + final V newVal = remappingFunction.apply(key, curr.getValuePlain()); + if (newVal != null) { + curr.setValueRelease(newVal); + return newVal; + } + + if (prev == null) { + ArrayUtil.setRelease(table, index, curr.getNextPlain()); + } else { + prev.setNextRelease(curr.getNextPlain()); + } + + this.removeFromSize(1); + + return null; + } + + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public V computeIfAbsent(final K key, final Function mappingFunction) { + Validate.notNull(key, "Null key"); + Validate.notNull(mappingFunction, "Null mappingFunction"); + + final int hash = SWMRHashTable.getHash(key); + final TableEntry[] table = this.getTablePlain(); + final int index = hash & (table.length - 1); + + for (TableEntry curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) { + if (curr != null) { + if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) { + return curr.getValuePlain(); + } + continue; + } + + final V newVal = mappingFunction.apply(key); + + if (newVal == null) { + return null; + } + + final TableEntry insert = new TableEntry<>(hash, key, newVal); + if (prev == null) { + ArrayUtil.setRelease(table, index, insert); + } else { + prev.setNextRelease(insert); + } + + this.addToSize(1); + + return newVal; + } + } + + /** + * {@inheritDoc} + */ + @Override + public V merge(final K key, final V value, final BiFunction remappingFunction) { + Validate.notNull(key, "Null key"); + Validate.notNull(value, "Null value"); + Validate.notNull(remappingFunction, "Null remappingFunction"); + + final int hash = SWMRHashTable.getHash(key); + final TableEntry[] table = this.getTablePlain(); + final int index = hash & (table.length - 1); + + for (TableEntry curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) { + if (curr == null) { + final TableEntry insert = new TableEntry<>(hash, key, value); + if (prev == null) { + ArrayUtil.setRelease(table, index, insert); + } else { + prev.setNextRelease(insert); + } + + this.addToSize(1); + + return value; + } + + if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) { + final V newVal = remappingFunction.apply(curr.getValuePlain(), value); + + if (newVal != null) { + curr.setValueRelease(newVal); + return newVal; + } + + if (prev == null) { + ArrayUtil.setRelease(table, index, curr.getNextPlain()); + } else { + prev.setNextRelease(curr.getNextPlain()); + } + + this.removeFromSize(1); + + return null; + } + } + } + + protected static final class TableEntry implements Map.Entry { + + protected final int hash; + protected final K key; + protected V value; + + protected TableEntry next; + + protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class); + protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class); + + /* value */ + + protected final V getValuePlain() { + //noinspection unchecked + return (V)VALUE_HANDLE.get(this); + } + + protected final V getValueAcquire() { + //noinspection unchecked + return (V)VALUE_HANDLE.getAcquire(this); + } + + protected final void setValueRelease(final V to) { + VALUE_HANDLE.setRelease(this, to); + } + + /* next */ + + protected final TableEntry getNextPlain() { + //noinspection unchecked + return (TableEntry)NEXT_HANDLE.get(this); + } + + protected final TableEntry getNextOpaque() { + //noinspection unchecked + return (TableEntry)NEXT_HANDLE.getOpaque(this); + } + + protected final void setNextPlain(final TableEntry next) { + NEXT_HANDLE.set(this, next); + } + + protected final void setNextRelease(final TableEntry next) { + NEXT_HANDLE.setRelease(this, next); + } + + protected TableEntry(final int hash, final K key, final V value) { + this.hash = hash; + this.key = key; + this.value = value; + } + + /** + * {@inheritDoc} + */ + @Override + public K getKey() { + return this.key; + } + + /** + * {@inheritDoc} + */ + @Override + public V getValue() { + return this.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + @Override + public V setValue(final V value) { + if (value == null) { + throw new NullPointerException(); + } + + final V curr = this.getValuePlain(); + + this.setValueRelease(value); + return curr; + } + + protected static int hash(final Object key, final Object value) { + return key.hashCode() ^ (value == null ? 0 : value.hashCode()); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return hash(this.key, this.getValueAcquire()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof Map.Entry)) { + return false; + } + final Map.Entry other = (Map.Entry)obj; + final Object otherKey = other.getKey(); + final Object otherValue = other.getValue(); + + final K thisKey = this.getKey(); + final V thisVal = this.getValueAcquire(); + return (thisKey == otherKey || thisKey.equals(otherKey)) && + (thisVal == otherValue || thisVal.equals(otherValue)); + } + } + + + protected static abstract class TableEntryIterator implements Iterator { + + protected final TableEntry[] table; + protected final SWMRHashTable map; + + /* bin which our current element resides on */ + protected int tableIndex; + + protected TableEntry currEntry; /* curr entry, null if no more to iterate or if curr was removed or if we've just init'd */ + protected TableEntry nextEntry; /* may not be on the same bin as currEntry */ + + protected TableEntryIterator(final TableEntry[] table, final SWMRHashTable map) { + this.table = table; + this.map = map; + int tableIndex = 0; + for (int len = table.length; tableIndex < len; ++tableIndex) { + final TableEntry entry = ArrayUtil.getOpaque(table, tableIndex); + if (entry != null) { + this.nextEntry = entry; + this.tableIndex = tableIndex + 1; + return; + } + } + this.tableIndex = tableIndex; + } + + @Override + public boolean hasNext() { + return this.nextEntry != null; + } + + protected final TableEntry advanceEntry() { + final TableEntry[] table = this.table; + final int tableLength = table.length; + int tableIndex = this.tableIndex; + final TableEntry curr = this.nextEntry; + if (curr == null) { + return null; + } + + this.currEntry = curr; + + // set up nextEntry + + // find next in chain + TableEntry next = curr.getNextOpaque(); + + if (next != null) { + this.nextEntry = next; + return curr; + } + + // nothing in chain, so find next available bin + for (;tableIndex < tableLength; ++tableIndex) { + next = ArrayUtil.getOpaque(table, tableIndex); + if (next != null) { + this.nextEntry = next; + this.tableIndex = tableIndex + 1; + return curr; + } + } + + this.nextEntry = null; + this.tableIndex = tableIndex; + return curr; + } + + @Override + public void remove() { + final TableEntry curr = this.currEntry; + if (curr == null) { + throw new IllegalStateException(); + } + + this.map.remove(curr.key, curr.hash); + + this.currEntry = null; + } + } + + protected static final class ValueIterator extends TableEntryIterator { + + protected ValueIterator(final TableEntry[] table, final SWMRHashTable map) { + super(table, map); + } + + @Override + public V next() { + final TableEntry entry = this.advanceEntry(); + + if (entry == null) { + throw new NoSuchElementException(); + } + + return entry.getValueAcquire(); + } + } + + protected static final class KeyIterator extends TableEntryIterator { + + protected KeyIterator(final TableEntry[] table, final SWMRHashTable map) { + super(table, map); + } + + @Override + public K next() { + final TableEntry curr = this.advanceEntry(); + + if (curr == null) { + throw new NoSuchElementException(); + } + + return curr.key; + } + } + + protected static final class EntryIterator extends TableEntryIterator> { + + protected EntryIterator(final TableEntry[] table, final SWMRHashTable map) { + super(table, map); + } + + @Override + public Map.Entry next() { + final TableEntry curr = this.advanceEntry(); + + if (curr == null) { + throw new NoSuchElementException(); + } + + return curr; + } + } + + protected static abstract class ViewCollection implements Collection { + + protected final SWMRHashTable map; + + protected ViewCollection(final SWMRHashTable map) { + this.map = map; + } + + @Override + public boolean add(final T element) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(final Collection collections) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(final Collection collection) { + Validate.notNull(collection, "Null collection"); + + boolean modified = false; + for (final Object element : collection) { + modified |= this.remove(element); + } + return modified; + } + + @Override + public int size() { + return this.map.size(); + } + + @Override + public boolean isEmpty() { + return this.size() == 0; + } + + @Override + public void clear() { + this.map.clear(); + } + + @Override + public boolean containsAll(final Collection collection) { + Validate.notNull(collection, "Null collection"); + + for (final Object element : collection) { + if (!this.contains(element)) { + return false; + } + } + + return true; + } + + @Override + public Object[] toArray() { + final List list = new ArrayList<>(this.size()); + + this.forEach(list::add); + + return list.toArray(); + } + + @Override + public E[] toArray(final E[] array) { + final List list = new ArrayList<>(this.size()); + + this.forEach(list::add); + + return list.toArray(array); + } + + @Override + public E[] toArray(final IntFunction generator) { + final List list = new ArrayList<>(this.size()); + + this.forEach(list::add); + + return list.toArray(generator); + } + + @Override + public int hashCode() { + int hash = 0; + for (final T element : this) { + hash += element == null ? 0 : element.hashCode(); + } + return hash; + } + + @Override + public Spliterator spliterator() { // TODO implement + return Spliterators.spliterator(this, Spliterator.NONNULL); + } + } + + protected static abstract class ViewSet extends ViewCollection implements Set { + + protected ViewSet(final SWMRHashTable map) { + super(map); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof Set)) { + return false; + } + + final Set other = (Set)obj; + if (other.size() != this.size()) { + return false; + } + + return this.containsAll(other); + } + } + + protected static final class EntrySet extends ViewSet> implements Set> { + + protected EntrySet(final SWMRHashTable map) { + super(map); + } + + @Override + public boolean remove(final Object object) { + if (!(object instanceof Map.Entry)) { + return false; + } + final Map.Entry entry = (Map.Entry)object; + + final Object key; + final Object value; + + try { + key = entry.getKey(); + value = entry.getValue(); + } catch (final IllegalStateException ex) { + return false; + } + + return this.map.remove(key, value); + } + + @Override + public boolean removeIf(final Predicate> filter) { + Validate.notNull(filter, "Null filter"); + + return this.map.removeEntryIf(filter) != 0; + } + + @Override + public boolean retainAll(final Collection collection) { + Validate.notNull(collection, "Null collection"); + + return this.map.removeEntryIf((final Map.Entry entry) -> { + return !collection.contains(entry); + }) != 0; + } + + @Override + public Iterator> iterator() { + return new EntryIterator<>(this.map.getTableAcquire(), this.map); + } + + @Override + public void forEach(final Consumer> action) { + this.map.forEach(action); + } + + @Override + public boolean contains(final Object object) { + if (!(object instanceof Map.Entry)) { + return false; + } + final Map.Entry entry = (Map.Entry)object; + + final Object key; + final Object value; + + try { + key = entry.getKey(); + value = entry.getValue(); + } catch (final IllegalStateException ex) { + return false; + } + + return this.map.contains(key, value); + } + + @Override + public String toString() { + return CollectionUtil.toString(this, "SWMRHashTableEntrySet"); + } + } + + protected static final class KeySet extends ViewSet { + + protected KeySet(final SWMRHashTable map) { + super(map); + } + + @Override + public Iterator iterator() { + return new KeyIterator<>(this.map.getTableAcquire(), this.map); + } + + @Override + public void forEach(final Consumer action) { + Validate.notNull(action, "Null action"); + + this.map.forEachKey(action); + } + + @Override + public boolean contains(final Object key) { + Validate.notNull(key, "Null key"); + + return this.map.containsKey(key); + } + + @Override + public boolean remove(final Object key) { + Validate.notNull(key, "Null key"); + + return this.map.remove(key) != null; + } + + @Override + public boolean retainAll(final Collection collection) { + Validate.notNull(collection, "Null collection"); + + return this.map.removeIf((final K key, final V value) -> { + return !collection.contains(key); + }) != 0; + } + + @Override + public boolean removeIf(final Predicate filter) { + Validate.notNull(filter, "Null filter"); + + return this.map.removeIf((final K key, final V value) -> { + return filter.test(key); + }) != 0; + } + + @Override + public String toString() { + return CollectionUtil.toString(this, "SWMRHashTableKeySet"); + } + } + + protected static final class ValueCollection extends ViewSet implements Collection { + + protected ValueCollection(final SWMRHashTable map) { + super(map); + } + + @Override + public Iterator iterator() { + return new ValueIterator<>(this.map.getTableAcquire(), this.map); + } + + @Override + public void forEach(final Consumer action) { + Validate.notNull(action, "Null action"); + + this.map.forEachValue(action); + } + + @Override + public boolean contains(final Object object) { + Validate.notNull(object, "Null object"); + + return this.map.containsValue(object); + } + + @Override + public boolean remove(final Object object) { + Validate.notNull(object, "Null object"); + + final Iterator itr = this.iterator(); + while (itr.hasNext()) { + final V val = itr.next(); + if (val == object || val.equals(object)) { + itr.remove(); + return true; + } + } + + return false; + } + + @Override + public boolean removeIf(final Predicate filter) { + Validate.notNull(filter, "Null filter"); + + return this.map.removeIf((final K key, final V value) -> { + return filter.test(value); + }) != 0; + } + + @Override + public boolean retainAll(final Collection collection) { + Validate.notNull(collection, "Null collection"); + + return this.map.removeIf((final K key, final V value) -> { + return !collection.contains(value); + }) != 0; + } + + @Override + public String toString() { + return CollectionUtil.toString(this, "SWMRHashTableValues"); + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRInt2IntHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRInt2IntHashTable.java new file mode 100644 index 0000000..7ae624a --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRInt2IntHashTable.java @@ -0,0 +1,664 @@ +package ca.spottedleaf.concurrentutil.map; + +import ca.spottedleaf.concurrentutil.util.ArrayUtil; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.IntegerUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import java.lang.invoke.VarHandle; +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public class SWMRInt2IntHashTable { + + protected int size; + + protected TableEntry[] table; + + protected final float loadFactor; + + protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRInt2IntHashTable.class, "size", int.class); + protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRInt2IntHashTable.class, "table", TableEntry[].class); + + /* size */ + + protected final int getSizePlain() { + return (int)SIZE_HANDLE.get(this); + } + + protected final int getSizeOpaque() { + return (int)SIZE_HANDLE.getOpaque(this); + } + + protected final int getSizeAcquire() { + return (int)SIZE_HANDLE.getAcquire(this); + } + + protected final void setSizePlain(final int value) { + SIZE_HANDLE.set(this, value); + } + + protected final void setSizeOpaque(final int value) { + SIZE_HANDLE.setOpaque(this, value); + } + + protected final void setSizeRelease(final int value) { + SIZE_HANDLE.setRelease(this, value); + } + + /* table */ + + protected final TableEntry[] getTablePlain() { + //noinspection unchecked + return (TableEntry[])TABLE_HANDLE.get(this); + } + + protected final TableEntry[] getTableAcquire() { + //noinspection unchecked + return (TableEntry[])TABLE_HANDLE.getAcquire(this); + } + + protected final void setTablePlain(final TableEntry[] table) { + TABLE_HANDLE.set(this, table); + } + + protected final void setTableRelease(final TableEntry[] table) { + TABLE_HANDLE.setRelease(this, table); + } + + protected static final int DEFAULT_CAPACITY = 16; + protected static final float DEFAULT_LOAD_FACTOR = 0.75f; + protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1; + + /** + * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}. + */ + public SWMRInt2IntHashTable() { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs this map with the specified capacity and load factor of {@code 0.75f}. + * @param capacity specified initial capacity, > 0 + */ + public SWMRInt2IntHashTable(final int capacity) { + this(capacity, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs this map with the specified capacity and load factor. + * @param capacity specified capacity, > 0 + * @param loadFactor specified load factor, > 0 && finite + */ + public SWMRInt2IntHashTable(final int capacity, final float loadFactor) { + final int tableSize = getCapacityFor(capacity); + + if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) { + throw new IllegalArgumentException("Invalid load factor: " + loadFactor); + } + + //noinspection unchecked + final TableEntry[] table = new TableEntry[tableSize]; + this.setTablePlain(table); + + if (tableSize == MAXIMUM_CAPACITY) { + this.threshold = -1; + } else { + this.threshold = getTargetCapacity(tableSize, loadFactor); + } + + this.loadFactor = loadFactor; + } + + /** + * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and + * with a load factor of {@code 0.75f}. + * All of the specified map's entries are copied into this map. + * @param other The specified map. + */ + public SWMRInt2IntHashTable(final SWMRInt2IntHashTable other) { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other); + } + + /** + * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and + * with a load factor of {@code 0.75f}. + * All of the specified map's entries are copied into this map. + * @param capacity specified capacity, > 0 + * @param other The specified map. + */ + public SWMRInt2IntHashTable(final int capacity, final SWMRInt2IntHashTable other) { + this(capacity, DEFAULT_LOAD_FACTOR, other); + } + + /** + * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and + * with the specified load factor. + * All of the specified map's entries are copied into this map. + * @param capacity specified capacity, > 0 + * @param loadFactor specified load factor, > 0 && finite + * @param other The specified map. + */ + public SWMRInt2IntHashTable(final int capacity, final float loadFactor, final SWMRInt2IntHashTable other) { + this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor); + this.putAll(other); + } + + public final float getLoadFactor() { + return this.loadFactor; + } + + protected static int getCapacityFor(final int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("Invalid capacity: " + capacity); + } + if (capacity >= MAXIMUM_CAPACITY) { + return MAXIMUM_CAPACITY; + } + return IntegerUtil.roundCeilLog2(capacity); + } + + /** Callers must still use acquire when reading the value of the entry. */ + protected final TableEntry getEntryForOpaque(final int key) { + final int hash = SWMRInt2IntHashTable.getHash(key); + final TableEntry[] table = this.getTableAcquire(); + + for (TableEntry curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) { + if (key == curr.key) { + return curr; + } + } + + return null; + } + + protected final TableEntry getEntryForPlain(final int key) { + final int hash = SWMRInt2IntHashTable.getHash(key); + final TableEntry[] table = this.getTablePlain(); + + for (TableEntry curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) { + if (key == curr.key) { + return curr; + } + } + + return null; + } + + /* MT-Safe */ + + /** must be deterministic given a key */ + protected static int getHash(final int key) { + return it.unimi.dsi.fastutil.HashCommon.mix(key); + } + + // rets -1 if capacity*loadFactor is too large + protected static int getTargetCapacity(final int capacity, final float loadFactor) { + final double ret = (double)capacity * (double)loadFactor; + if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) { + return -1; + } + + return (int)ret; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + /* Make no attempt to deal with concurrent modifications */ + if (!(obj instanceof SWMRInt2IntHashTable)) { + return false; + } + final SWMRInt2IntHashTable other = (SWMRInt2IntHashTable)obj; + + if (this.size() != other.size()) { + return false; + } + + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final int value = curr.getValueAcquire(); + + final int otherValue = other.get(curr.key); + if (value != otherValue) { + return false; + } + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + /* Make no attempt to deal with concurrent modifications */ + int hash = 0; + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + hash += curr.hashCode(); + } + } + + return hash; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(64); + builder.append("SingleWriterMultiReaderHashMap:{"); + + this.forEach((final int key, final int value) -> { + builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}"); + }); + + return builder.append('}').toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public SWMRInt2IntHashTable clone() { + return new SWMRInt2IntHashTable(this.getTableAcquire().length, this.loadFactor, this); + } + + /** + * {@inheritDoc} + */ + public void forEach(final Consumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr); + } + } + } + + @FunctionalInterface + public static interface BiIntIntConsumer { + public void accept(final int key, final int value); + } + + /** + * {@inheritDoc} + */ + public void forEach(final BiIntIntConsumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final int value = curr.getValueAcquire(); + + action.accept(curr.key, value); + } + } + } + + /** + * Provides the specified consumer with all keys contained within this map. + * @param action The specified consumer. + */ + public void forEachKey(final IntConsumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr.key); + } + } + } + + /** + * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}. + * @param action The specified consumer. + */ + public void forEachValue(final IntConsumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final int value = curr.getValueAcquire(); + + action.accept(value); + } + } + } + + /** + * {@inheritDoc} + */ + public int get(final int key) { + final TableEntry entry = this.getEntryForOpaque(key); + return entry == null ? 0 : entry.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + public boolean containsKey(final int key) { + final TableEntry entry = this.getEntryForOpaque(key); + return entry != null; + } + + /** + * {@inheritDoc} + */ + public int getOrDefault(final int key, final int defaultValue) { + final TableEntry entry = this.getEntryForOpaque(key); + + return entry == null ? defaultValue : entry.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + public int size() { + return this.getSizeAcquire(); + } + + /** + * {@inheritDoc} + */ + public boolean isEmpty() { + return this.getSizeAcquire() == 0; + } + + /* Non-MT-Safe */ + + protected int threshold; + + protected final void checkResize(final int minCapacity) { + if (minCapacity <= this.threshold || this.threshold < 0) { + return; + } + + final TableEntry[] table = this.getTablePlain(); + int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity); + if (newCapacity < 0) { + newCapacity = MAXIMUM_CAPACITY; + } + if (newCapacity <= table.length) { + if (newCapacity == MAXIMUM_CAPACITY) { + return; + } + newCapacity = table.length << 1; + } + + //noinspection unchecked + final TableEntry[] newTable = new TableEntry[newCapacity]; + final int indexMask = newCapacity - 1; + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry entry = table[i]; entry != null; entry = entry.getNextPlain()) { + final int key = entry.key; + final int hash = SWMRInt2IntHashTable.getHash(key); + final int index = hash & indexMask; + + /* we need to create a new entry since there could be reading threads */ + final TableEntry insert = new TableEntry(key, entry.getValuePlain()); + + final TableEntry prev = newTable[index]; + + newTable[index] = insert; + insert.setNextPlain(prev); + } + } + + if (newCapacity == MAXIMUM_CAPACITY) { + this.threshold = -1; /* No more resizing */ + } else { + this.threshold = getTargetCapacity(newCapacity, this.loadFactor); + } + this.setTableRelease(newTable); /* use release to publish entries in table */ + } + + protected final int addToSize(final int num) { + final int newSize = this.getSizePlain() + num; + + this.setSizeOpaque(newSize); + this.checkResize(newSize); + + return newSize; + } + + protected final int removeFromSize(final int num) { + final int newSize = this.getSizePlain() - num; + + this.setSizeOpaque(newSize); + + return newSize; + } + + protected final int put(final int key, final int value, final boolean onlyIfAbsent) { + final TableEntry[] table = this.getTablePlain(); + final int hash = SWMRInt2IntHashTable.getHash(key); + final int index = hash & (table.length - 1); + + final TableEntry head = table[index]; + if (head == null) { + final TableEntry insert = new TableEntry(key, value); + ArrayUtil.setRelease(table, index, insert); + this.addToSize(1); + return 0; + } + + for (TableEntry curr = head;;) { + if (key == curr.key) { + if (onlyIfAbsent) { + return curr.getValuePlain(); + } + + final int currVal = curr.getValuePlain(); + curr.setValueRelease(value); + return currVal; + } + + final TableEntry next = curr.getNextPlain(); + if (next != null) { + curr = next; + continue; + } + + final TableEntry insert = new TableEntry(key, value); + + curr.setNextRelease(insert); + this.addToSize(1); + return 0; + } + } + + /** + * {@inheritDoc} + */ + public int put(final int key, final int value) { + return this.put(key, value, false); + } + + /** + * {@inheritDoc} + */ + public int putIfAbsent(final int key, final int value) { + return this.put(key, value, true); + } + + protected final int remove(final int key, final int hash) { + final TableEntry[] table = this.getTablePlain(); + final int index = (table.length - 1) & hash; + + final TableEntry head = table[index]; + if (head == null) { + return 0; + } + + if (head.key == key) { + ArrayUtil.setRelease(table, index, head.getNextPlain()); + this.removeFromSize(1); + + return head.getValuePlain(); + } + + for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { + if (key == curr.key) { + prev.setNextRelease(curr.getNextPlain()); + this.removeFromSize(1); + + return curr.getValuePlain(); + } + } + + return 0; + } + + /** + * {@inheritDoc} + */ + public int remove(final int key) { + return this.remove(key, SWMRInt2IntHashTable.getHash(key)); + } + + /** + * {@inheritDoc} + */ + public void putAll(final SWMRInt2IntHashTable map) { + Validate.notNull(map, "Null map"); + + final int size = map.size(); + this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */ + map.forEach(this::put); + } + + /** + * {@inheritDoc} + *

+ * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself + * is release ordered, that is, after the clear operation is performed a release fence is performed. + *

+ */ + public void clear() { + Arrays.fill(this.getTablePlain(), null); + this.setSizeRelease(0); + } + + public static final class TableEntry { + + protected final int key; + protected int value; + + protected TableEntry next; + + protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class); + protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class); + + /* value */ + + protected final int getValuePlain() { + //noinspection unchecked + return (int)VALUE_HANDLE.get(this); + } + + protected final int getValueAcquire() { + //noinspection unchecked + return (int)VALUE_HANDLE.getAcquire(this); + } + + protected final void setValueRelease(final int to) { + VALUE_HANDLE.setRelease(this, to); + } + + /* next */ + + protected final TableEntry getNextPlain() { + //noinspection unchecked + return (TableEntry)NEXT_HANDLE.get(this); + } + + protected final TableEntry getNextOpaque() { + //noinspection unchecked + return (TableEntry)NEXT_HANDLE.getOpaque(this); + } + + protected final void setNextPlain(final TableEntry next) { + NEXT_HANDLE.set(this, next); + } + + protected final void setNextRelease(final TableEntry next) { + NEXT_HANDLE.setRelease(this, next); + } + + protected TableEntry(final int key, final int value) { + this.key = key; + this.value = value; + } + + public int getKey() { + return this.key; + } + + public int getValue() { + return this.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + public int setValue(final int value) { + final int curr = this.getValuePlain(); + + this.setValueRelease(value); + return curr; + } + + protected static int hash(final int key, final int value) { + return SWMRInt2IntHashTable.getHash(key) ^ SWMRInt2IntHashTable.getHash(value); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return hash(this.key, this.getValueAcquire()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof TableEntry)) { + return false; + } + final TableEntry other = (TableEntry)obj; + final int otherKey = other.getKey(); + final int thisKey = this.getKey(); + final int otherValue = other.getValueAcquire(); + final int thisVal = this.getValueAcquire(); + return (thisKey == otherKey) && (thisVal == otherValue); + } + } + +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java new file mode 100644 index 0000000..990b580 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java @@ -0,0 +1,714 @@ +package ca.spottedleaf.concurrentutil.map; + +import ca.spottedleaf.concurrentutil.util.ArrayUtil; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.IntegerUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import java.lang.invoke.VarHandle; +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.function.LongConsumer; + +// trimmed down version of SWMRHashTable +public class SWMRLong2ObjectHashTable { + + protected int size; + + protected TableEntry[] table; + + protected final float loadFactor; + + protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRLong2ObjectHashTable.class, "size", int.class); + protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRLong2ObjectHashTable.class, "table", TableEntry[].class); + + /* size */ + + protected final int getSizePlain() { + return (int)SIZE_HANDLE.get(this); + } + + protected final int getSizeOpaque() { + return (int)SIZE_HANDLE.getOpaque(this); + } + + protected final int getSizeAcquire() { + return (int)SIZE_HANDLE.getAcquire(this); + } + + protected final void setSizePlain(final int value) { + SIZE_HANDLE.set(this, value); + } + + protected final void setSizeOpaque(final int value) { + SIZE_HANDLE.setOpaque(this, value); + } + + protected final void setSizeRelease(final int value) { + SIZE_HANDLE.setRelease(this, value); + } + + /* table */ + + protected final TableEntry[] getTablePlain() { + //noinspection unchecked + return (TableEntry[])TABLE_HANDLE.get(this); + } + + protected final TableEntry[] getTableAcquire() { + //noinspection unchecked + return (TableEntry[])TABLE_HANDLE.getAcquire(this); + } + + protected final void setTablePlain(final TableEntry[] table) { + TABLE_HANDLE.set(this, table); + } + + protected final void setTableRelease(final TableEntry[] table) { + TABLE_HANDLE.setRelease(this, table); + } + + protected static final int DEFAULT_CAPACITY = 16; + protected static final float DEFAULT_LOAD_FACTOR = 0.75f; + protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1; + + /** + * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}. + */ + public SWMRLong2ObjectHashTable() { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs this map with the specified capacity and load factor of {@code 0.75f}. + * @param capacity specified initial capacity, > 0 + */ + public SWMRLong2ObjectHashTable(final int capacity) { + this(capacity, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs this map with the specified capacity and load factor. + * @param capacity specified capacity, > 0 + * @param loadFactor specified load factor, > 0 && finite + */ + public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor) { + final int tableSize = getCapacityFor(capacity); + + if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) { + throw new IllegalArgumentException("Invalid load factor: " + loadFactor); + } + + //noinspection unchecked + final TableEntry[] table = new TableEntry[tableSize]; + this.setTablePlain(table); + + if (tableSize == MAXIMUM_CAPACITY) { + this.threshold = -1; + } else { + this.threshold = getTargetCapacity(tableSize, loadFactor); + } + + this.loadFactor = loadFactor; + } + + /** + * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and + * with a load factor of {@code 0.75f}. + * All of the specified map's entries are copied into this map. + * @param other The specified map. + */ + public SWMRLong2ObjectHashTable(final SWMRLong2ObjectHashTable other) { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other); + } + + /** + * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and + * with a load factor of {@code 0.75f}. + * All of the specified map's entries are copied into this map. + * @param capacity specified capacity, > 0 + * @param other The specified map. + */ + public SWMRLong2ObjectHashTable(final int capacity, final SWMRLong2ObjectHashTable other) { + this(capacity, DEFAULT_LOAD_FACTOR, other); + } + + /** + * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and + * with the specified load factor. + * All of the specified map's entries are copied into this map. + * @param capacity specified capacity, > 0 + * @param loadFactor specified load factor, > 0 && finite + * @param other The specified map. + */ + public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor, final SWMRLong2ObjectHashTable other) { + this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor); + this.putAll(other); + } + + public final float getLoadFactor() { + return this.loadFactor; + } + + protected static int getCapacityFor(final int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("Invalid capacity: " + capacity); + } + if (capacity >= MAXIMUM_CAPACITY) { + return MAXIMUM_CAPACITY; + } + return IntegerUtil.roundCeilLog2(capacity); + } + + /** Callers must still use acquire when reading the value of the entry. */ + protected final TableEntry getEntryForOpaque(final long key) { + final int hash = SWMRLong2ObjectHashTable.getHash(key); + final TableEntry[] table = this.getTableAcquire(); + + for (TableEntry curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) { + if (key == curr.key) { + return curr; + } + } + + return null; + } + + protected final TableEntry getEntryForPlain(final long key) { + final int hash = SWMRLong2ObjectHashTable.getHash(key); + final TableEntry[] table = this.getTablePlain(); + + for (TableEntry curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) { + if (key == curr.key) { + return curr; + } + } + + return null; + } + + /* MT-Safe */ + + /** must be deterministic given a key */ + protected static int getHash(final long key) { + return (int)it.unimi.dsi.fastutil.HashCommon.mix(key); + } + + // rets -1 if capacity*loadFactor is too large + protected static int getTargetCapacity(final int capacity, final float loadFactor) { + final double ret = (double)capacity * (double)loadFactor; + if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) { + return -1; + } + + return (int)ret; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + /* Make no attempt to deal with concurrent modifications */ + if (!(obj instanceof SWMRLong2ObjectHashTable)) { + return false; + } + final SWMRLong2ObjectHashTable other = (SWMRLong2ObjectHashTable)obj; + + if (this.size() != other.size()) { + return false; + } + + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + final Object otherValue = other.get(curr.key); + if (otherValue == null || (value != otherValue && value.equals(otherValue))) { + return false; + } + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + /* Make no attempt to deal with concurrent modifications */ + int hash = 0; + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + hash += curr.hashCode(); + } + } + + return hash; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(64); + builder.append("SingleWriterMultiReaderHashMap:{"); + + this.forEach((final long key, final V value) -> { + builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}"); + }); + + return builder.append('}').toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public SWMRLong2ObjectHashTable clone() { + return new SWMRLong2ObjectHashTable<>(this.getTableAcquire().length, this.loadFactor, this); + } + + /** + * {@inheritDoc} + */ + public void forEach(final Consumer> action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr); + } + } + } + + @FunctionalInterface + public static interface BiLongObjectConsumer { + public void accept(final long key, final V value); + } + + /** + * {@inheritDoc} + */ + public void forEach(final BiLongObjectConsumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + action.accept(curr.key, value); + } + } + } + + /** + * Provides the specified consumer with all keys contained within this map. + * @param action The specified consumer. + */ + public void forEachKey(final LongConsumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr.key); + } + } + } + + /** + * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}. + * @param action The specified consumer. + */ + public void forEachValue(final Consumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + action.accept(value); + } + } + } + + /** + * {@inheritDoc} + */ + public V get(final long key) { + final TableEntry entry = this.getEntryForOpaque(key); + return entry == null ? null : entry.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + public boolean containsKey(final long key) { + // note: we need to use getValueAcquire, so that the reads from this map are ordered by acquire semantics + return this.get(key) != null; + } + + /** + * {@inheritDoc} + */ + public V getOrDefault(final long key, final V defaultValue) { + final TableEntry entry = this.getEntryForOpaque(key); + + return entry == null ? defaultValue : entry.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + public int size() { + return this.getSizeAcquire(); + } + + /** + * {@inheritDoc} + */ + public boolean isEmpty() { + return this.getSizeAcquire() == 0; + } + + /* Non-MT-Safe */ + + protected int threshold; + + protected final void checkResize(final int minCapacity) { + if (minCapacity <= this.threshold || this.threshold < 0) { + return; + } + + final TableEntry[] table = this.getTablePlain(); + int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity); + if (newCapacity < 0) { + newCapacity = MAXIMUM_CAPACITY; + } + if (newCapacity <= table.length) { + if (newCapacity == MAXIMUM_CAPACITY) { + return; + } + newCapacity = table.length << 1; + } + + //noinspection unchecked + final TableEntry[] newTable = new TableEntry[newCapacity]; + final int indexMask = newCapacity - 1; + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry entry = table[i]; entry != null; entry = entry.getNextPlain()) { + final long key = entry.key; + final int hash = SWMRLong2ObjectHashTable.getHash(key); + final int index = hash & indexMask; + + /* we need to create a new entry since there could be reading threads */ + final TableEntry insert = new TableEntry<>(key, entry.getValuePlain()); + + final TableEntry prev = newTable[index]; + + newTable[index] = insert; + insert.setNextPlain(prev); + } + } + + if (newCapacity == MAXIMUM_CAPACITY) { + this.threshold = -1; /* No more resizing */ + } else { + this.threshold = getTargetCapacity(newCapacity, this.loadFactor); + } + this.setTableRelease(newTable); /* use release to publish entries in table */ + } + + protected final int addToSize(final int num) { + final int newSize = this.getSizePlain() + num; + + this.setSizeOpaque(newSize); + this.checkResize(newSize); + + return newSize; + } + + protected final int removeFromSize(final int num) { + final int newSize = this.getSizePlain() - num; + + this.setSizeOpaque(newSize); + + return newSize; + } + + protected final V put(final long key, final V value, final boolean onlyIfAbsent) { + final TableEntry[] table = this.getTablePlain(); + final int hash = SWMRLong2ObjectHashTable.getHash(key); + final int index = hash & (table.length - 1); + + final TableEntry head = table[index]; + if (head == null) { + final TableEntry insert = new TableEntry<>(key, value); + ArrayUtil.setRelease(table, index, insert); + this.addToSize(1); + return null; + } + + for (TableEntry curr = head;;) { + if (key == curr.key) { + if (onlyIfAbsent) { + return curr.getValuePlain(); + } + + final V currVal = curr.getValuePlain(); + curr.setValueRelease(value); + return currVal; + } + + final TableEntry next = curr.getNextPlain(); + if (next != null) { + curr = next; + continue; + } + + final TableEntry insert = new TableEntry<>(key, value); + + curr.setNextRelease(insert); + this.addToSize(1); + return null; + } + } + + /** + * {@inheritDoc} + */ + public V put(final long key, final V value) { + Validate.notNull(value, "Null value"); + + return this.put(key, value, false); + } + + /** + * {@inheritDoc} + */ + public V putIfAbsent(final long key, final V value) { + Validate.notNull(value, "Null value"); + + return this.put(key, value, true); + } + + protected final V remove(final long key, final int hash) { + final TableEntry[] table = this.getTablePlain(); + final int index = (table.length - 1) & hash; + + final TableEntry head = table[index]; + if (head == null) { + return null; + } + + if (head.key == key) { + ArrayUtil.setRelease(table, index, head.getNextPlain()); + this.removeFromSize(1); + + return head.getValuePlain(); + } + + for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { + if (key == curr.key) { + prev.setNextRelease(curr.getNextPlain()); + this.removeFromSize(1); + + return curr.getValuePlain(); + } + } + + return null; + } + + protected final V remove(final long key, final int hash, final V expect) { + final TableEntry[] table = this.getTablePlain(); + final int index = (table.length - 1) & hash; + + final TableEntry head = table[index]; + if (head == null) { + return null; + } + + if (head.key == key) { + final V val = head.value; + if (val == expect || val.equals(expect)) { + ArrayUtil.setRelease(table, index, head.getNextPlain()); + this.removeFromSize(1); + + return head.getValuePlain(); + } else { + return null; + } + } + + for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { + if (key == curr.key) { + final V val = curr.value; + if (val == expect || val.equals(expect)) { + prev.setNextRelease(curr.getNextPlain()); + this.removeFromSize(1); + + return curr.getValuePlain(); + } else { + return null; + } + } + } + + return null; + } + + /** + * {@inheritDoc} + */ + public V remove(final long key) { + return this.remove(key, SWMRLong2ObjectHashTable.getHash(key)); + } + + public boolean remove(final long key, final V expect) { + return this.remove(key, SWMRLong2ObjectHashTable.getHash(key), expect) != null; + } + + /** + * {@inheritDoc} + */ + public void putAll(final SWMRLong2ObjectHashTable map) { + Validate.notNull(map, "Null map"); + + final int size = map.size(); + this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */ + map.forEach(this::put); + } + + /** + * {@inheritDoc} + *

+ * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself + * is release ordered, that is, after the clear operation is performed a release fence is performed. + *

+ */ + public void clear() { + Arrays.fill(this.getTablePlain(), null); + this.setSizeRelease(0); + } + + public static final class TableEntry { + + protected final long key; + protected V value; + + protected TableEntry next; + + protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class); + protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class); + + /* value */ + + protected final V getValuePlain() { + //noinspection unchecked + return (V)VALUE_HANDLE.get(this); + } + + protected final V getValueAcquire() { + //noinspection unchecked + return (V)VALUE_HANDLE.getAcquire(this); + } + + protected final void setValueRelease(final V to) { + VALUE_HANDLE.setRelease(this, to); + } + + /* next */ + + protected final TableEntry getNextPlain() { + //noinspection unchecked + return (TableEntry)NEXT_HANDLE.get(this); + } + + protected final TableEntry getNextOpaque() { + //noinspection unchecked + return (TableEntry)NEXT_HANDLE.getOpaque(this); + } + + protected final void setNextPlain(final TableEntry next) { + NEXT_HANDLE.set(this, next); + } + + protected final void setNextRelease(final TableEntry next) { + NEXT_HANDLE.setRelease(this, next); + } + + protected TableEntry(final long key, final V value) { + this.key = key; + this.value = value; + } + + public long getKey() { + return this.key; + } + + public V getValue() { + return this.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + public V setValue(final V value) { + if (value == null) { + throw new NullPointerException(); + } + + final V curr = this.getValuePlain(); + + this.setValueRelease(value); + return curr; + } + + protected static int hash(final long key, final Object value) { + return SWMRLong2ObjectHashTable.getHash(key) ^ (value == null ? 0 : value.hashCode()); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return hash(this.key, this.getValueAcquire()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof TableEntry)) { + return false; + } + final TableEntry other = (TableEntry)obj; + final long otherKey = other.getKey(); + final long thisKey = this.getKey(); + final Object otherValue = other.getValueAcquire(); + final V thisVal = this.getValueAcquire(); + return (thisKey == otherKey) && (thisVal == otherValue || thisVal.equals(otherValue)); + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java new file mode 100644 index 0000000..83ea3c9 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java @@ -0,0 +1,534 @@ +package ca.spottedleaf.concurrentutil.scheduler; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.TimeUtil; +import ca.spottedleaf.concurrentutil.set.LinkedSortedSet; +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; +import java.lang.invoke.VarHandle; +import java.util.BitSet; +import java.util.Comparator; +import java.util.PriorityQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.LockSupport; +import java.util.function.BooleanSupplier; + +public class SchedulerThreadPool { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public static final long DEADLINE_NOT_SET = Long.MIN_VALUE; + + private static final Comparator TICK_COMPARATOR_BY_TIME = (final SchedulableTick t1, final SchedulableTick t2) -> { + final int timeCompare = TimeUtil.compareTimes(t1.scheduledStart, t2.scheduledStart); + if (timeCompare != 0) { + return timeCompare; + } + + return Long.compare(t1.id, t2.id); + }; + + private final TickThreadRunner[] runners; + private final Thread[] threads; + private final LinkedSortedSet awaiting = new LinkedSortedSet<>(TICK_COMPARATOR_BY_TIME); + private final PriorityQueue queued = new PriorityQueue<>(TICK_COMPARATOR_BY_TIME); + private final BitSet idleThreads; + + private final Object scheduleLock = new Object(); + + private volatile boolean halted; + + public SchedulerThreadPool(final int threads, final ThreadFactory threadFactory) { + final BitSet idleThreads = new BitSet(threads); + for (int i = 0; i < threads; ++i) { + idleThreads.set(i); + } + this.idleThreads = idleThreads; + + final TickThreadRunner[] runners = new TickThreadRunner[threads]; + final Thread[] t = new Thread[threads]; + for (int i = 0; i < threads; ++i) { + runners[i] = new TickThreadRunner(i, this); + t[i] = threadFactory.newThread(runners[i]); + } + + this.threads = t; + this.runners = runners; + } + + /** + * Starts the threads in this pool. + */ + public void start() { + for (final Thread thread : this.threads) { + thread.start(); + } + } + + /** + * Attempts to prevent further execution of tasks, optionally waiting for the scheduler threads to die. + * + * @param sync Whether to wait for the scheduler threads to die. + * @param maxWaitNS The maximum time, in ns, to wait for the scheduler threads to die. + * @return {@code true} if sync was false, or if sync was true and the scheduler threads died before the timeout. + * Otherwise, returns {@code false} if the time elapsed exceeded the maximum wait time. + */ + public boolean halt(final boolean sync, final long maxWaitNS) { + this.halted = true; + for (final Thread thread : this.threads) { + // force response to halt + LockSupport.unpark(thread); + } + final long time = System.nanoTime(); + if (sync) { + // start at 10 * 0.5ms -> 5ms + for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { + boolean allDead = true; + for (final Thread thread : this.threads) { + if (thread.isAlive()) { + allDead = false; + break; + } + } + if (allDead) { + return true; + } + if ((System.nanoTime() - time) >= maxWaitNS) { + return false; + } + } + } + + return true; + } + + /** + * Returns an array of the underlying scheduling threads. + */ + public Thread[] getThreads() { + return this.threads.clone(); + } + + private void insertFresh(final SchedulableTick task) { + final TickThreadRunner[] runners = this.runners; + + final int firstIdleThread = this.idleThreads.nextSetBit(0); + + if (firstIdleThread != -1) { + // push to idle thread + this.idleThreads.clear(firstIdleThread); + final TickThreadRunner runner = runners[firstIdleThread]; + task.awaitingLink = this.awaiting.addLast(task); + runner.acceptTask(task); + return; + } + + // try to replace the last awaiting task + final SchedulableTick last = this.awaiting.last(); + + if (last != null && TICK_COMPARATOR_BY_TIME.compare(task, last) < 0) { + // need to replace the last task + this.awaiting.pollLast(); + last.awaitingLink = null; + task.awaitingLink = this.awaiting.addLast(task); + // need to add task to queue to be picked up later + this.queued.add(last); + + final TickThreadRunner runner = last.ownedBy; + runner.replaceTask(task); + + return; + } + + // add to queue, will be picked up later + this.queued.add(task); + } + + private void takeTask(final TickThreadRunner runner, final SchedulableTick tick) { + if (!this.awaiting.remove(tick.awaitingLink)) { + throw new IllegalStateException("Task is not in awaiting"); + } + tick.awaitingLink = null; + } + + private SchedulableTick returnTask(final TickThreadRunner runner, final SchedulableTick reschedule) { + if (reschedule != null) { + this.queued.add(reschedule); + } + final SchedulableTick ret = this.queued.poll(); + if (ret == null) { + this.idleThreads.set(runner.id); + } else { + ret.awaitingLink = this.awaiting.addLast(ret); + } + + return ret; + } + + public void schedule(final SchedulableTick task) { + synchronized (this.scheduleLock) { + if (!task.tryMarkScheduled()) { + throw new IllegalStateException("Task " + task + " is already scheduled or cancelled"); + } + + task.schedulerOwnedBy = this; + + this.insertFresh(task); + } + } + + public boolean updateTickStartToMax(final SchedulableTick task, final long newStart) { + synchronized (this.scheduleLock) { + if (TimeUtil.compareTimes(newStart, task.getScheduledStart()) <= 0) { + return false; + } + if (this.queued.remove(task)) { + task.setScheduledStart(newStart); + this.queued.add(task); + return true; + } + if (task.awaitingLink != null) { + this.awaiting.remove(task.awaitingLink); + task.awaitingLink = null; + + // re-queue task + task.setScheduledStart(newStart); + this.queued.add(task); + + // now we need to replace the task the runner was waiting for + final TickThreadRunner runner = task.ownedBy; + final SchedulableTick replace = this.queued.poll(); + + // replace cannot be null, since we have added a task to queued + if (replace != task) { + runner.replaceTask(replace); + } + + return true; + } + + return false; + } + } + + /** + * Returns {@code null} if the task is not scheduled, returns {@code TRUE} if the task was cancelled + * and was queued to execute, returns {@code FALSE} if the task was cancelled but was executing. + */ + public Boolean tryRetire(final SchedulableTick task) { + if (task.schedulerOwnedBy != this) { + return null; + } + + synchronized (this.scheduleLock) { + if (this.queued.remove(task)) { + // cancelled, and no runner owns it - so return + return Boolean.TRUE; + } + if (task.awaitingLink != null) { + this.awaiting.remove(task.awaitingLink); + task.awaitingLink = null; + // here we need to replace the task the runner was waiting for + final TickThreadRunner runner = task.ownedBy; + final SchedulableTick replace = this.queued.poll(); + + if (replace == null) { + // nothing to replace with, set to idle + this.idleThreads.set(runner.id); + runner.forceIdle(); + } else { + runner.replaceTask(replace); + } + + return Boolean.TRUE; + } + + // could not find it in queue + return task.tryMarkCancelled() ? Boolean.FALSE : null; + } + } + + public void notifyTasks(final SchedulableTick task) { + // Not implemented + } + + /** + * Represents a tickable task that can be scheduled into a {@link SchedulerThreadPool}. + *

+ * A tickable task is expected to run on a fixed interval, which is determined by + * the {@link SchedulerThreadPool}. + *

+ *

+ * A tickable task can have intermediate tasks that can be executed before its tick method is ran. Instead of + * the {@link SchedulerThreadPool} parking in-between ticks, the scheduler will instead drain + * intermediate tasks from scheduled tasks. The parsing of intermediate tasks allows the scheduler to take + * advantage of downtime to reduce the intermediate task load from tasks once they begin ticking. + *

+ *

+ * It is guaranteed that {@link #runTick()} and {@link #runTasks(BooleanSupplier)} are never + * invoked in parallel. + * It is required that when intermediate tasks are scheduled, that {@link SchedulerThreadPool#notifyTasks(SchedulableTick)} + * is invoked for any scheduled task - otherwise, {@link #runTasks(BooleanSupplier)} may not be invoked to + * parse intermediate tasks. + *

+ */ + public static abstract class SchedulableTick { + private static final AtomicLong ID_GENERATOR = new AtomicLong(); + public final long id = ID_GENERATOR.getAndIncrement(); + + private static final int SCHEDULE_STATE_NOT_SCHEDULED = 0; + private static final int SCHEDULE_STATE_SCHEDULED = 1; + private static final int SCHEDULE_STATE_CANCELLED = 2; + + private final AtomicInteger scheduled = new AtomicInteger(); + private SchedulerThreadPool schedulerOwnedBy; + private long scheduledStart = DEADLINE_NOT_SET; + private TickThreadRunner ownedBy; + + private LinkedSortedSet.Link awaitingLink; + + private boolean tryMarkScheduled() { + return this.scheduled.compareAndSet(SCHEDULE_STATE_NOT_SCHEDULED, SCHEDULE_STATE_SCHEDULED); + } + + private boolean tryMarkCancelled() { + return this.scheduled.compareAndSet(SCHEDULE_STATE_SCHEDULED, SCHEDULE_STATE_CANCELLED); + } + + private boolean isScheduled() { + return this.scheduled.get() == SCHEDULE_STATE_SCHEDULED; + } + + protected final long getScheduledStart() { + return this.scheduledStart; + } + + /** + * If this task is scheduled, then this may only be invoked during {@link #runTick()}, + * and {@link #runTasks(BooleanSupplier)} + */ + protected final void setScheduledStart(final long value) { + this.scheduledStart = value; + } + + /** + * Executes the tick. + *

+ * It is the callee's responsibility to invoke {@link #setScheduledStart(long)} to adjust the start of + * the next tick. + *

+ * @return {@code true} if the task should continue to be scheduled, {@code false} otherwise. + */ + public abstract boolean runTick(); + + /** + * Returns whether this task has any intermediate tasks that can be executed. + */ + public abstract boolean hasTasks(); + + /** + * Returns {@code null} if this task should not be scheduled, otherwise returns + * {@code Boolean.TRUE} if there are more intermediate tasks to execute and + * {@code Boolean.FALSE} if there are no more intermediate tasks to execute. + */ + public abstract Boolean runTasks(final BooleanSupplier canContinue); + + @Override + public String toString() { + return "SchedulableTick:{" + + "class=" + this.getClass().getName() + "," + + "scheduled_state=" + this.scheduled.get() + "," + + "}"; + } + } + + private static final class TickThreadRunner implements Runnable { + + /** + * There are no tasks in this thread's runqueue, so it is parked. + *

+ * stateTarget = null + *

+ */ + private static final int STATE_IDLE = 0; + + /** + * The runner is waiting to tick a task, as it has no intermediate tasks to execute. + *

+ * stateTarget = the task awaiting tick + *

+ */ + private static final int STATE_AWAITING_TICK = 1; + + /** + * The runner is executing a tick for one of the tasks that was in its runqueue. + *

+ * stateTarget = the task being ticked + *

+ */ + private static final int STATE_EXECUTING_TICK = 2; + + public final int id; + public final SchedulerThreadPool scheduler; + + private volatile Thread thread; + private volatile TickThreadRunnerState state = new TickThreadRunnerState(null, STATE_IDLE); + private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(TickThreadRunner.class, "state", TickThreadRunnerState.class); + + private void setStatePlain(final TickThreadRunnerState state) { + STATE_HANDLE.set(this, state); + } + + private void setStateOpaque(final TickThreadRunnerState state) { + STATE_HANDLE.setOpaque(this, state); + } + + private void setStateVolatile(final TickThreadRunnerState state) { + STATE_HANDLE.setVolatile(this, state); + } + + private static record TickThreadRunnerState(SchedulableTick stateTarget, int state) {} + + public TickThreadRunner(final int id, final SchedulerThreadPool scheduler) { + this.id = id; + this.scheduler = scheduler; + } + + private Thread getRunnerThread() { + return this.thread; + } + + private void acceptTask(final SchedulableTick task) { + if (task.ownedBy != null) { + throw new IllegalStateException("Already owned by another runner"); + } + task.ownedBy = this; + final TickThreadRunnerState state = this.state; + if (state.state != STATE_IDLE) { + throw new IllegalStateException("Cannot accept task in state " + state); + } + this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK)); + LockSupport.unpark(this.getRunnerThread()); + } + + private void replaceTask(final SchedulableTick task) { + final TickThreadRunnerState state = this.state; + if (state.state != STATE_AWAITING_TICK) { + throw new IllegalStateException("Cannot replace task in state " + state); + } + if (task.ownedBy != null) { + throw new IllegalStateException("Already owned by another runner"); + } + task.ownedBy = this; + + state.stateTarget.ownedBy = null; + + this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK)); + LockSupport.unpark(this.getRunnerThread()); + } + + private void forceIdle() { + final TickThreadRunnerState state = this.state; + if (state.state != STATE_AWAITING_TICK) { + throw new IllegalStateException("Cannot replace task in state " + state); + } + state.stateTarget.ownedBy = null; + this.setStateOpaque(new TickThreadRunnerState(null, STATE_IDLE)); + // no need to unpark + } + + private boolean takeTask(final TickThreadRunnerState state, final SchedulableTick task) { + synchronized (this.scheduler.scheduleLock) { + if (this.state != state) { + return false; + } + this.setStatePlain(new TickThreadRunnerState(task, STATE_EXECUTING_TICK)); + this.scheduler.takeTask(this, task); + return true; + } + } + + private void returnTask(final SchedulableTick task, final boolean reschedule) { + synchronized (this.scheduler.scheduleLock) { + task.ownedBy = null; + + final SchedulableTick newWait = this.scheduler.returnTask(this, reschedule && task.isScheduled() ? task : null); + if (newWait == null) { + this.setStatePlain(new TickThreadRunnerState(null, STATE_IDLE)); + } else { + if (newWait.ownedBy != null) { + throw new IllegalStateException("Already owned by another runner"); + } + newWait.ownedBy = this; + this.setStatePlain(new TickThreadRunnerState(newWait, STATE_AWAITING_TICK)); + } + } + } + + @Override + public void run() { + this.thread = Thread.currentThread(); + + main_state_loop: + for (;;) { + final TickThreadRunnerState startState = this.state; + final int startStateType = startState.state; + final SchedulableTick startStateTask = startState.stateTarget; + + if (this.scheduler.halted) { + return; + } + + switch (startStateType) { + case STATE_IDLE: { + while (this.state.state == STATE_IDLE) { + LockSupport.park(); + if (this.scheduler.halted) { + return; + } + } + continue main_state_loop; + } + + case STATE_AWAITING_TICK: { + final long deadline = startStateTask.getScheduledStart(); + for (;;) { + if (this.state != startState) { + continue main_state_loop; + } + final long diff = deadline - System.nanoTime(); + if (diff <= 0L) { + break; + } + LockSupport.parkNanos(startState, diff); + if (this.scheduler.halted) { + return; + } + } + + if (!this.takeTask(startState, startStateTask)) { + continue main_state_loop; + } + + // TODO exception handling + final boolean reschedule = startStateTask.runTick(); + + this.returnTask(startStateTask, reschedule); + + continue main_state_loop; + } + + case STATE_EXECUTING_TICK: { + throw new IllegalStateException("Tick execution must be set by runner thread, not by any other thread"); + } + + default: { + throw new IllegalStateException("Unknown state: " + startState); + } + } + } + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java new file mode 100644 index 0000000..98adb7e --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java @@ -0,0 +1,274 @@ +package ca.spottedleaf.concurrentutil.set; + + +import java.util.Comparator; +import java.util.Iterator; +import java.util.NoSuchElementException; + +// TODO rebase into util patch +public final class LinkedSortedSet implements Iterable { + + public final Comparator comparator; + + protected Link head; + protected Link tail; + + public LinkedSortedSet() { + this((Comparator)Comparator.naturalOrder()); + } + + public LinkedSortedSet(final Comparator comparator) { + this.comparator = comparator; + } + + public void clear() { + this.head = this.tail = null; + } + + public boolean isEmpty() { + return this.head == null; + } + + public E first() { + final Link head = this.head; + return head == null ? null : head.element; + } + + public E last() { + final Link tail = this.tail; + return tail == null ? null : tail.element; + } + + public boolean containsFirst(final E element) { + final Comparator comparator = this.comparator; + for (Link curr = this.head; curr != null; curr = curr.next) { + if (comparator.compare(element, curr.element) == 0) { + return true; + } + } + return false; + } + + public boolean containsLast(final E element) { + final Comparator comparator = this.comparator; + for (Link curr = this.tail; curr != null; curr = curr.prev) { + if (comparator.compare(element, curr.element) == 0) { + return true; + } + } + return false; + } + + private void removeNode(final Link node) { + final Link prev = node.prev; + final Link next = node.next; + + // help GC + node.element = null; + node.prev = null; + node.next = null; + + if (prev == null) { + this.head = next; + } else { + prev.next = next; + } + + if (next == null) { + this.tail = prev; + } else { + next.prev = prev; + } + } + + public boolean remove(final Link link) { + if (link.element == null) { + return false; + } + + this.removeNode(link); + return true; + } + + public boolean removeFirst(final E element) { + final Comparator comparator = this.comparator; + for (Link curr = this.head; curr != null; curr = curr.next) { + if (comparator.compare(element, curr.element) == 0) { + this.removeNode(curr); + return true; + } + } + return false; + } + + public boolean removeLast(final E element) { + final Comparator comparator = this.comparator; + for (Link curr = this.tail; curr != null; curr = curr.prev) { + if (comparator.compare(element, curr.element) == 0) { + this.removeNode(curr); + return true; + } + } + return false; + } + + @Override + public Iterator iterator() { + return new Iterator<>() { + private Link next = LinkedSortedSet.this.head; + + @Override + public boolean hasNext() { + return this.next != null; + } + + @Override + public E next() { + final Link next = this.next; + if (next == null) { + throw new NoSuchElementException(); + } + this.next = next.next; + return next.element; + } + }; + } + + public E pollFirst() { + final Link head = this.head; + if (head == null) { + return null; + } + + final E ret = head.element; + final Link next = head.next; + + // unlink head + this.head = next; + if (next == null) { + this.tail = null; + } else { + next.prev = null; + } + + // help GC + head.element = null; + head.next = null; + + return ret; + } + + public E pollLast() { + final Link tail = this.tail; + if (tail == null) { + return null; + } + + final E ret = tail.element; + final Link prev = tail.prev; + + // unlink tail + this.tail = prev; + if (prev == null) { + this.head = null; + } else { + prev.next = null; + } + + // help GC + tail.element = null; + tail.prev = null; + + return ret; + } + + public Link addLast(final E element) { + final Comparator comparator = this.comparator; + + Link curr = this.tail; + if (curr != null) { + int compare; + + while ((compare = comparator.compare(element, curr.element)) < 0) { + Link prev = curr; + curr = curr.prev; + if (curr != null) { + continue; + } + return this.head = prev.prev = new Link<>(element, null, prev); + } + + if (compare != 0) { + // insert after curr + final Link next = curr.next; + final Link insert = new Link<>(element, curr, next); + curr.next = insert; + + if (next == null) { + this.tail = insert; + } else { + next.prev = insert; + } + return insert; + } + + return null; + } else { + return this.head = this.tail = new Link<>(element); + } + } + + public Link addFirst(final E element) { + final Comparator comparator = this.comparator; + + Link curr = this.head; + if (curr != null) { + int compare; + + while ((compare = comparator.compare(element, curr.element)) > 0) { + Link prev = curr; + curr = curr.next; + if (curr != null) { + continue; + } + return this.tail = prev.next = new Link<>(element, prev, null); + } + + if (compare != 0) { + // insert before curr + final Link prev = curr.prev; + final Link insert = new Link<>(element, prev, curr); + curr.prev = insert; + + if (prev == null) { + this.head = insert; + } else { + prev.next = insert; + } + return insert; + } + + return null; + } else { + return this.head = this.tail = new Link<>(element); + } + } + + public static final class Link { + private E element; + private Link prev; + private Link next; + + private Link() {} + + private Link(final E element) { + this.element = element; + } + + private Link(final E element, final Link prev, final Link next) { + this.element = element; + this.prev = prev; + this.next = next; + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java new file mode 100644 index 0000000..ebb1ab0 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java @@ -0,0 +1,816 @@ +package ca.spottedleaf.concurrentutil.util; + +import java.lang.invoke.VarHandle; + +public final class ArrayUtil { + + public static final VarHandle BOOLEAN_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(boolean[].class); + + public static final VarHandle BYTE_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(byte[].class); + + public static final VarHandle SHORT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(short[].class); + + public static final VarHandle INT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(int[].class); + + public static final VarHandle LONG_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(long[].class); + + public static final VarHandle OBJECT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(Object[].class); + + private ArrayUtil() { + throw new RuntimeException(); + } + + /* byte array */ + + public static byte getPlain(final byte[] array, final int index) { + return (byte)BYTE_ARRAY_HANDLE.get(array, index); + } + + public static byte getOpaque(final byte[] array, final int index) { + return (byte)BYTE_ARRAY_HANDLE.getOpaque(array, index); + } + + public static byte getAcquire(final byte[] array, final int index) { + return (byte)BYTE_ARRAY_HANDLE.getAcquire(array, index); + } + + public static byte getVolatile(final byte[] array, final int index) { + return (byte)BYTE_ARRAY_HANDLE.getVolatile(array, index); + } + + public static void setPlain(final byte[] array, final int index, final byte value) { + BYTE_ARRAY_HANDLE.set(array, index, value); + } + + public static void setOpaque(final byte[] array, final int index, final byte value) { + BYTE_ARRAY_HANDLE.setOpaque(array, index, value); + } + + public static void setRelease(final byte[] array, final int index, final byte value) { + BYTE_ARRAY_HANDLE.setRelease(array, index, value); + } + + public static void setVolatile(final byte[] array, final int index, final byte value) { + BYTE_ARRAY_HANDLE.setVolatile(array, index, value); + } + + public static void setVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + public static byte compareAndExchangeVolatile(final byte[] array, final int index, final byte expect, final byte update) { + return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static byte getAndAddVolatile(final byte[] array, final int index, final byte param) { + return (byte)BYTE_ARRAY_HANDLE.getAndAdd(array, index, param); + } + + public static byte getAndAndVolatile(final byte[] array, final int index, final byte param) { + return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); + } + + public static byte getAndOrVolatile(final byte[] array, final int index, final byte param) { + return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); + } + + public static byte getAndXorVolatile(final byte[] array, final int index, final byte param) { + return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); + } + + public static byte getAndSetVolatile(final byte[] array, final int index, final byte param) { + return (byte)BYTE_ARRAY_HANDLE.getAndSet(array, index, param); + } + + public static byte compareAndExchangeVolatileContended(final byte[] array, final int index, final byte expect, final byte update) { + return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static byte getAndAddVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr + param)))) { + return curr; + } + } + } + + public static byte getAndAndVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr & param)))) { + return curr; + } + } + } + + public static byte getAndOrVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr | param)))) { + return curr; + } + } + } + + public static byte getAndXorVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr ^ param)))) { + return curr; + } + } + } + + public static byte getAndSetVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } + + /* short array */ + + public static short getPlain(final short[] array, final int index) { + return (short)SHORT_ARRAY_HANDLE.get(array, index); + } + + public static short getOpaque(final short[] array, final int index) { + return (short)SHORT_ARRAY_HANDLE.getOpaque(array, index); + } + + public static short getAcquire(final short[] array, final int index) { + return (short)SHORT_ARRAY_HANDLE.getAcquire(array, index); + } + + public static short getVolatile(final short[] array, final int index) { + return (short)SHORT_ARRAY_HANDLE.getVolatile(array, index); + } + + public static void setPlain(final short[] array, final int index, final short value) { + SHORT_ARRAY_HANDLE.set(array, index, value); + } + + public static void setOpaque(final short[] array, final int index, final short value) { + SHORT_ARRAY_HANDLE.setOpaque(array, index, value); + } + + public static void setRelease(final short[] array, final int index, final short value) { + SHORT_ARRAY_HANDLE.setRelease(array, index, value); + } + + public static void setVolatile(final short[] array, final int index, final short value) { + SHORT_ARRAY_HANDLE.setVolatile(array, index, value); + } + + public static void setVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + public static short compareAndExchangeVolatile(final short[] array, final int index, final short expect, final short update) { + return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static short getAndAddVolatile(final short[] array, final int index, final short param) { + return (short)SHORT_ARRAY_HANDLE.getAndAdd(array, index, param); + } + + public static short getAndAndVolatile(final short[] array, final int index, final short param) { + return (short)SHORT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); + } + + public static short getAndOrVolatile(final short[] array, final int index, final short param) { + return (short)SHORT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); + } + + public static short getAndXorVolatile(final short[] array, final int index, final short param) { + return (short)SHORT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); + } + + public static short getAndSetVolatile(final short[] array, final int index, final short param) { + return (short)SHORT_ARRAY_HANDLE.getAndSet(array, index, param); + } + + public static short compareAndExchangeVolatileContended(final short[] array, final int index, final short expect, final short update) { + return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static short getAndAddVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr + param)))) { + return curr; + } + } + } + + public static short getAndAndVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr & param)))) { + return curr; + } + } + } + + public static short getAndOrVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr | param)))) { + return curr; + } + } + } + + public static short getAndXorVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr ^ param)))) { + return curr; + } + } + } + + public static short getAndSetVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } + + /* int array */ + + public static int getPlain(final int[] array, final int index) { + return (int)INT_ARRAY_HANDLE.get(array, index); + } + + public static int getOpaque(final int[] array, final int index) { + return (int)INT_ARRAY_HANDLE.getOpaque(array, index); + } + + public static int getAcquire(final int[] array, final int index) { + return (int)INT_ARRAY_HANDLE.getAcquire(array, index); + } + + public static int getVolatile(final int[] array, final int index) { + return (int)INT_ARRAY_HANDLE.getVolatile(array, index); + } + + public static void setPlain(final int[] array, final int index, final int value) { + INT_ARRAY_HANDLE.set(array, index, value); + } + + public static void setOpaque(final int[] array, final int index, final int value) { + INT_ARRAY_HANDLE.setOpaque(array, index, value); + } + + public static void setRelease(final int[] array, final int index, final int value) { + INT_ARRAY_HANDLE.setRelease(array, index, value); + } + + public static void setVolatile(final int[] array, final int index, final int value) { + INT_ARRAY_HANDLE.setVolatile(array, index, value); + } + + public static void setVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + public static int compareAndExchangeVolatile(final int[] array, final int index, final int expect, final int update) { + return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static int getAndAddVolatile(final int[] array, final int index, final int param) { + return (int)INT_ARRAY_HANDLE.getAndAdd(array, index, param); + } + + public static int getAndAndVolatile(final int[] array, final int index, final int param) { + return (int)INT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); + } + + public static int getAndOrVolatile(final int[] array, final int index, final int param) { + return (int)INT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); + } + + public static int getAndXorVolatile(final int[] array, final int index, final int param) { + return (int)INT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); + } + + public static int getAndSetVolatile(final int[] array, final int index, final int param) { + return (int)INT_ARRAY_HANDLE.getAndSet(array, index, param); + } + + public static int compareAndExchangeVolatileContended(final int[] array, final int index, final int expect, final int update) { + return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static int getAndAddVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr + param)))) { + return curr; + } + } + } + + public static int getAndAndVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr & param)))) { + return curr; + } + } + } + + public static int getAndOrVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr | param)))) { + return curr; + } + } + } + + public static int getAndXorVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr ^ param)))) { + return curr; + } + } + } + + public static int getAndSetVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } + + /* long array */ + + public static long getPlain(final long[] array, final int index) { + return (long)LONG_ARRAY_HANDLE.get(array, index); + } + + public static long getOpaque(final long[] array, final int index) { + return (long)LONG_ARRAY_HANDLE.getOpaque(array, index); + } + + public static long getAcquire(final long[] array, final int index) { + return (long)LONG_ARRAY_HANDLE.getAcquire(array, index); + } + + public static long getVolatile(final long[] array, final int index) { + return (long)LONG_ARRAY_HANDLE.getVolatile(array, index); + } + + public static void setPlain(final long[] array, final int index, final long value) { + LONG_ARRAY_HANDLE.set(array, index, value); + } + + public static void setOpaque(final long[] array, final int index, final long value) { + LONG_ARRAY_HANDLE.setOpaque(array, index, value); + } + + public static void setRelease(final long[] array, final int index, final long value) { + LONG_ARRAY_HANDLE.setRelease(array, index, value); + } + + public static void setVolatile(final long[] array, final int index, final long value) { + LONG_ARRAY_HANDLE.setVolatile(array, index, value); + } + + public static void setVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + public static long compareAndExchangeVolatile(final long[] array, final int index, final long expect, final long update) { + return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static long getAndAddVolatile(final long[] array, final int index, final long param) { + return (long)LONG_ARRAY_HANDLE.getAndAdd(array, index, param); + } + + public static long getAndAndVolatile(final long[] array, final int index, final long param) { + return (long)LONG_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); + } + + public static long getAndOrVolatile(final long[] array, final int index, final long param) { + return (long)LONG_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); + } + + public static long getAndXorVolatile(final long[] array, final int index, final long param) { + return (long)LONG_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); + } + + public static long getAndSetVolatile(final long[] array, final int index, final long param) { + return (long)LONG_ARRAY_HANDLE.getAndSet(array, index, param); + } + + public static long compareAndExchangeVolatileContended(final long[] array, final int index, final long expect, final long update) { + return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static long getAndAddVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr + param)))) { + return curr; + } + } + } + + public static long getAndAndVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr & param)))) { + return curr; + } + } + } + + public static long getAndOrVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr | param)))) { + return curr; + } + } + } + + public static long getAndXorVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr ^ param)))) { + return curr; + } + } + } + + public static long getAndSetVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } + + /* boolean array */ + + public static boolean getPlain(final boolean[] array, final int index) { + return (boolean)BOOLEAN_ARRAY_HANDLE.get(array, index); + } + + public static boolean getOpaque(final boolean[] array, final int index) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getOpaque(array, index); + } + + public static boolean getAcquire(final boolean[] array, final int index) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getAcquire(array, index); + } + + public static boolean getVolatile(final boolean[] array, final int index) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getVolatile(array, index); + } + + public static void setPlain(final boolean[] array, final int index, final boolean value) { + BOOLEAN_ARRAY_HANDLE.set(array, index, value); + } + + public static void setOpaque(final boolean[] array, final int index, final boolean value) { + BOOLEAN_ARRAY_HANDLE.setOpaque(array, index, value); + } + + public static void setRelease(final boolean[] array, final int index, final boolean value) { + BOOLEAN_ARRAY_HANDLE.setRelease(array, index, value); + } + + public static void setVolatile(final boolean[] array, final int index, final boolean value) { + BOOLEAN_ARRAY_HANDLE.setVolatile(array, index, value); + } + + public static void setVolatileContended(final boolean[] array, final int index, final boolean param) { + int failures = 0; + + for (boolean curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + public static boolean compareAndExchangeVolatile(final boolean[] array, final int index, final boolean expect, final boolean update) { + return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static boolean getAndOrVolatile(final boolean[] array, final int index, final boolean param) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); + } + + public static boolean getAndXorVolatile(final boolean[] array, final int index, final boolean param) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); + } + + public static boolean getAndSetVolatile(final boolean[] array, final int index, final boolean param) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getAndSet(array, index, param); + } + + public static boolean compareAndExchangeVolatileContended(final boolean[] array, final int index, final boolean expect, final boolean update) { + return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static boolean getAndAndVolatileContended(final boolean[] array, final int index, final boolean param) { + int failures = 0; + + for (boolean curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr & param)))) { + return curr; + } + } + } + + public static boolean getAndOrVolatileContended(final boolean[] array, final int index, final boolean param) { + int failures = 0; + + for (boolean curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr | param)))) { + return curr; + } + } + } + + public static boolean getAndXorVolatileContended(final boolean[] array, final int index, final boolean param) { + int failures = 0; + + for (boolean curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr ^ param)))) { + return curr; + } + } + } + + public static boolean getAndSetVolatileContended(final boolean[] array, final int index, final boolean param) { + int failures = 0; + + for (boolean curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } + + @SuppressWarnings("unchecked") + public static T getPlain(final T[] array, final int index) { + final Object ret = OBJECT_ARRAY_HANDLE.get((Object[])array, index); + return (T)ret; + } + + @SuppressWarnings("unchecked") + public static T getOpaque(final T[] array, final int index) { + final Object ret = OBJECT_ARRAY_HANDLE.getOpaque((Object[])array, index); + return (T)ret; + } + + @SuppressWarnings("unchecked") + public static T getAcquire(final T[] array, final int index) { + final Object ret = OBJECT_ARRAY_HANDLE.getAcquire((Object[])array, index); + return (T)ret; + } + + @SuppressWarnings("unchecked") + public static T getVolatile(final T[] array, final int index) { + final Object ret = OBJECT_ARRAY_HANDLE.getVolatile((Object[])array, index); + return (T)ret; + } + + public static void setPlain(final T[] array, final int index, final T value) { + OBJECT_ARRAY_HANDLE.set((Object[])array, index, (Object)value); + } + + public static void setOpaque(final T[] array, final int index, final T value) { + OBJECT_ARRAY_HANDLE.setOpaque((Object[])array, index, (Object)value); + } + + public static void setRelease(final T[] array, final int index, final T value) { + OBJECT_ARRAY_HANDLE.setRelease((Object[])array, index, (Object)value); + } + + public static void setVolatile(final T[] array, final int index, final T value) { + OBJECT_ARRAY_HANDLE.setVolatile((Object[])array, index, (Object)value); + } + + public static void setVolatileContended(final T[] array, final int index, final T param) { + int failures = 0; + + for (T curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + @SuppressWarnings("unchecked") + public static T compareAndExchangeVolatile(final T[] array, final int index, final T expect, final T update) { + final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update); + return (T)ret; + } + + @SuppressWarnings("unchecked") + public static T getAndSetVolatile(final T[] array, final int index, final T param) { + final Object ret = BYTE_ARRAY_HANDLE.getAndSet((Object[])array, index, (Object)param); + return (T)ret; + } + + @SuppressWarnings("unchecked") + public static T compareAndExchangeVolatileContended(final T[] array, final int index, final T expect, final T update) { + final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update); + return (T)ret; + } + + public static T getAndSetVolatileContended(final T[] array, final int index, final T param) { + int failures = 0; + + for (T curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java new file mode 100644 index 0000000..9420b98 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java @@ -0,0 +1,31 @@ +package ca.spottedleaf.concurrentutil.util; + +import java.util.Collection; + +public final class CollectionUtil { + + public static String toString(final Collection collection, final String name) { + return CollectionUtil.toString(collection, name, new StringBuilder(name.length() + 128)).toString(); + } + + public static StringBuilder toString(final Collection collection, final String name, final StringBuilder builder) { + builder.append(name).append("{elements={"); + + boolean first = true; + + for (final Object element : collection) { + if (!first) { + builder.append(", "); + } + first = false; + + builder.append('"').append(element).append('"'); + } + + return builder.append("}}"); + } + + private CollectionUtil() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java new file mode 100644 index 0000000..23ae82e --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java @@ -0,0 +1,166 @@ +package ca.spottedleaf.concurrentutil.util; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.concurrent.locks.LockSupport; + +public final class ConcurrentUtil { + + public static String genericToString(final Object object) { + return object == null ? "null" : object.getClass().getName() + ":" + object.hashCode() + ":" + object.toString(); + } + + public static void rethrow(Throwable exception) { + rethrow0(exception); + } + + private static void rethrow0(Throwable thr) throws T { + throw (T)thr; + } + + public static VarHandle getVarHandle(final Class lookIn, final String fieldName, final Class fieldType) { + try { + return MethodHandles.privateLookupIn(lookIn, MethodHandles.lookup()).findVarHandle(lookIn, fieldName, fieldType); + } catch (final Exception ex) { + throw new RuntimeException(ex); // unreachable + } + } + + public static VarHandle getStaticVarHandle(final Class lookIn, final String fieldName, final Class fieldType) { + try { + return MethodHandles.privateLookupIn(lookIn, MethodHandles.lookup()).findStaticVarHandle(lookIn, fieldName, fieldType); + } catch (final Exception ex) { + throw new RuntimeException(ex); // unreachable + } + } + + /** + * Non-exponential backoff algorithm to use in lightly contended areas. + * @see ConcurrentUtil#exponentiallyBackoffSimple(long) + * @see ConcurrentUtil#exponentiallyBackoffComplex(long) + */ + public static void backoff() { + Thread.onSpinWait(); + } + + /** + * Backoff algorithm to use for a short held lock (i.e compareAndExchange operation). Generally this should not be + * used when a thread can block another thread. Instead, use {@link ConcurrentUtil#exponentiallyBackoffComplex(long)}. + * @param counter The current counter. + * @return The counter plus 1. + * @see ConcurrentUtil#backoff() + * @see ConcurrentUtil#exponentiallyBackoffComplex(long) + */ + public static long exponentiallyBackoffSimple(final long counter) { + for (long i = 0; i < counter; ++i) { + backoff(); + } + return counter + 1L; + } + + /** + * Backoff algorithm to use for a lock that can block other threads (i.e if another thread contending with this thread + * can be thrown off the scheduler). This lock should not be used for simple locks such as compareAndExchange. + * @param counter The current counter. + * @return The next (if any) step in the backoff logic. + * @see ConcurrentUtil#backoff() + * @see ConcurrentUtil#exponentiallyBackoffSimple(long) + */ + public static long exponentiallyBackoffComplex(final long counter) { + // TODO experimentally determine counters + if (counter < 100L) { + return exponentiallyBackoffSimple(counter); + } + if (counter < 1_200L) { + Thread.yield(); + LockSupport.parkNanos(1_000L); + return counter + 1L; + } + // scale 0.1ms (100us) per failure + Thread.yield(); + LockSupport.parkNanos(100_000L * counter); + return counter + 1; + } + + /** + * Simple exponential backoff that will linearly increase the time per failure, according to the scale. + * @param counter The current failure counter. + * @param scale Time per failure, in ns. + * @param max The maximum time to wait for, in ns. + * @return The next counter. + */ + public static long linearLongBackoff(long counter, final long scale, long max) { + counter = Math.min(Long.MAX_VALUE, counter + 1); // prevent overflow + max = Math.max(0, max); + + if (scale <= 0L) { + return counter; + } + + long time = scale * counter; + + if (time > max || time / scale != counter) { + time = max; + } + + boolean interrupted = Thread.interrupted(); + if (time > 1_000_000L) { // 1ms + Thread.yield(); + } + LockSupport.parkNanos(time); + if (interrupted) { + Thread.currentThread().interrupt(); + } + return counter; + } + + /** + * Simple exponential backoff that will linearly increase the time per failure, according to the scale. + * @param counter The current failure counter. + * @param scale Time per failure, in ns. + * @param max The maximum time to wait for, in ns. + * @param deadline The deadline in ns. Deadline time source: {@link System#nanoTime()}. + * @return The next counter. + */ + public static long linearLongBackoffDeadline(long counter, final long scale, long max, long deadline) { + counter = Math.min(Long.MAX_VALUE, counter + 1); // prevent overflow + max = Math.max(0, max); + + if (scale <= 0L) { + return counter; + } + + long time = scale * counter; + + // check overflow + if (time / scale != counter) { + // overflew + --counter; + time = max; + } else if (time > max) { + time = max; + } + + final long currTime = System.nanoTime(); + final long diff = deadline - currTime; + if (diff <= 0) { + return counter; + } + if (diff <= 1_500_000L) { // 1.5ms + time = 100_000L; // 100us + } else if (time > 1_000_000L) { // 1ms + Thread.yield(); + } + + boolean interrupted = Thread.interrupted(); + LockSupport.parkNanos(time); + if (interrupted) { + Thread.currentThread().interrupt(); + } + return counter; + } + + public static VarHandle getArrayHandle(final Class type) { + return MethodHandles.arrayElementVarHandle(type); + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java new file mode 100644 index 0000000..144189f --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java @@ -0,0 +1,243 @@ +package ca.spottedleaf.concurrentutil.util; + + +public final class IntegerUtil { + + public static final int HIGH_BIT_U32 = Integer.MIN_VALUE; + public static final long HIGH_BIT_U64 = Long.MIN_VALUE; + + public static int ceilLog2(final int value) { + return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros + } + + public static long ceilLog2(final long value) { + return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros + } + + public static int floorLog2(final int value) { + // xor is optimized subtract for 2^n -1 + // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1) + return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros + } + + public static int floorLog2(final long value) { + // xor is optimized subtract for 2^n -1 + // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1) + return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros + } + + public static int roundCeilLog2(final int value) { + // optimized variant of 1 << (32 - leading(val - 1)) + // given + // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32) + // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1))) + // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1))) + // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1)) + // HIGH_BIT_32 >>> (-1 + leading(val - 1)) + return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1); + } + + public static long roundCeilLog2(final long value) { + // see logic documented above + return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1); + } + + public static int roundFloorLog2(final int value) { + // optimized variant of 1 << (31 - leading(val)) + // given + // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32) + // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val))) + // HIGH_BIT_32 >> (31 - (31 - leading(val))) + // HIGH_BIT_32 >> (31 - 31 + leading(val)) + return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value); + } + + public static long roundFloorLog2(final long value) { + // see logic documented above + return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value); + } + + public static boolean isPowerOfTwo(final int n) { + // 2^n has one bit + // note: this rets true for 0 still + return IntegerUtil.getTrailingBit(n) == n; + } + + public static boolean isPowerOfTwo(final long n) { + // 2^n has one bit + // note: this rets true for 0 still + return IntegerUtil.getTrailingBit(n) == n; + } + + public static int getTrailingBit(final int n) { + return -n & n; + } + + public static long getTrailingBit(final long n) { + return -n & n; + } + + public static int trailingZeros(final int n) { + return Integer.numberOfTrailingZeros(n); + } + + public static int trailingZeros(final long n) { + return Long.numberOfTrailingZeros(n); + } + + // from hacker's delight (signed division magic value) + public static int getDivisorMultiple(final long numbers) { + return (int)(numbers >>> 32); + } + + // from hacker's delight (signed division magic value) + public static int getDivisorShift(final long numbers) { + return (int)numbers; + } + + // copied from hacker's delight (signed division magic value) + // http://www.hackersdelight.org/hdcodetxt/magic.c.txt + public static long getDivisorNumbers(final int d) { + final int ad = branchlessAbs(d); + + if (ad < 2) { + throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d); + } + + final int two31 = 0x80000000; + final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour + + /* + Signed usage: + int number; + long magic = getDivisorNumbers(div); + long mul = magic >>> 32; + int sign = number >> 31; + int result = (int)(((long)number * mul) >>> magic) - sign; + */ + /* + Unsigned usage: (note: fails for input > Integer.MAX_VALUE, only use when input < Integer.MAX_VALUE to avoid sign calculation) + int number; + long magic = getDivisorNumbers(div); + long mul = magic >>> 32; + int result = (int)(((long)number * mul) >>> magic); + */ + + int p = 31; + + // all these variables are UNSIGNED! + int t = two31 + (d >>> 31); + int anc = t - 1 - (int)((t & mask)%ad); + int q1 = (int)((two31 & mask)/(anc & mask)); + int r1 = two31 - q1*anc; + int q2 = (int)((two31 & mask)/(ad & mask)); + int r2 = two31 - q2*ad; + int delta; + + do { + p = p + 1; + q1 = 2*q1; // Update q1 = 2**p/|nc|. + r1 = 2*r1; // Update r1 = rem(2**p, |nc|). + if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here) + q1 = q1 + 1; + r1 = r1 - anc; + } + q2 = 2*q2; // Update q2 = 2**p/|d|. + r2 = 2*r2; // Update r2 = rem(2**p, |d|). + if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here) + q2 = q2 + 1; + r2 = r2 - ad; + } + delta = ad - r2; + } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0)); + + int magicNum = q2 + 1; + if (d < 0) { + magicNum = -magicNum; + } + int shift = p; + return ((long)magicNum << 32) | shift; + } + + public static int branchlessAbs(final int val) { + // -n = -1 ^ n + 1 + final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0 + return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1 + } + + public static long branchlessAbs(final long val) { + // -n = -1 ^ n + 1 + final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0 + return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1 + } + + //https://github.com/skeeto/hash-prospector for hash functions + + //score = ~590.47984224483832 + public static int hash0(int x) { + x *= 0x36935555; + x ^= x >>> 16; + return x; + } + + //score = ~310.01596637036749 + public static int hash1(int x) { + x ^= x >>> 15; + x *= 0x356aaaad; + x ^= x >>> 17; + return x; + } + + public static int hash2(int x) { + x ^= x >>> 16; + x *= 0x7feb352d; + x ^= x >>> 15; + x *= 0x846ca68b; + x ^= x >>> 16; + return x; + } + + public static int hash3(int x) { + x ^= x >>> 17; + x *= 0xed5ad4bb; + x ^= x >>> 11; + x *= 0xac4c1b51; + x ^= x >>> 15; + x *= 0x31848bab; + x ^= x >>> 14; + return x; + } + + //score = ~365.79959673201887 + public static long hash1(long x) { + x ^= x >>> 27; + x *= 0xb24924b71d2d354bL; + x ^= x >>> 28; + return x; + } + + //h2 hash + public static long hash2(long x) { + x ^= x >>> 32; + x *= 0xd6e8feb86659fd93L; + x ^= x >>> 32; + x *= 0xd6e8feb86659fd93L; + x ^= x >>> 32; + return x; + } + + public static long hash3(long x) { + x ^= x >>> 45; + x *= 0xc161abe5704b6c79L; + x ^= x >>> 41; + x *= 0xe3e5389aedbc90f7L; + x ^= x >>> 56; + x *= 0x1f9aba75a52db073L; + x ^= x >>> 53; + return x; + } + + private IntegerUtil() { + throw new RuntimeException(); + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java new file mode 100644 index 0000000..6368871 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java @@ -0,0 +1,60 @@ +package ca.spottedleaf.concurrentutil.util; + +public final class TimeUtil { + + /* + * The comparator is not a valid comparator for every long value. To prove where it is valid, see below. + * + * For reflexivity, we have that x - x = 0. We then have that for any long value x that + * compareTimes(x, x) == 0, as expected. + * + * For symmetry, we have that x - y = -(y - x) except for when y - x = Long.MIN_VALUE. + * So, the difference between any times x and y must not be equal to Long.MIN_VALUE. + * + * As for the transitive relation, consider we have x,y such that x - y = a > 0 and z such that + * y - z = b > 0. Then, we will have that the x - z > 0 is equivalent to a + b > 0. For long values, + * this holds as long as a + b <= Long.MAX_VALUE. + * + * Also consider we have x, y such that x - y = a < 0 and z such that y - z = b < 0. Then, we will have + * that x - z < 0 is equivalent to a + b < 0. For long values, this holds as long as a + b >= -Long.MAX_VALUE. + * + * Thus, the comparator is only valid for timestamps such that abs(c - d) <= Long.MAX_VALUE for all timestamps + * c and d. + */ + + /** + * This function is appropriate to be used as a {@link java.util.Comparator} between two timestamps, which + * indicates whether the timestamps represented by t1, t2 that t1 is before, equal to, or after t2. + */ + public static int compareTimes(final long t1, final long t2) { + final long diff = t1 - t2; + + // HD, Section 2-7 + return (int) ((diff >> 63) | (-diff >>> 63)); + } + + public static long getGreatestTime(final long t1, final long t2) { + final long diff = t1 - t2; + return diff < 0L ? t2 : t1; + } + + public static long getLeastTime(final long t1, final long t2) { + final long diff = t1 - t2; + return diff > 0L ? t2 : t1; + } + + public static long clampTime(final long value, final long min, final long max) { + final long diffMax = value - max; + final long diffMin = value - min; + + if (diffMax > 0L) { + return max; + } + if (diffMin < 0L) { + return min; + } + return value; + } + + private TimeUtil() {} +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java new file mode 100644 index 0000000..382177d --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java @@ -0,0 +1,28 @@ +package ca.spottedleaf.concurrentutil.util; + +public final class Validate { + + public static T notNull(final T obj) { + if (obj == null) { + throw new NullPointerException(); + } + return obj; + } + + public static T notNull(final T obj, final String msgIfNull) { + if (obj == null) { + throw new NullPointerException(msgIfNull); + } + return obj; + } + + public static void arrayBounds(final int off, final int len, final int arrayLength, final String msgPrefix) { + if (off < 0 || len < 0 || (arrayLength - off) < len) { + throw new ArrayIndexOutOfBoundsException(msgPrefix + ": off: " + off + ", len: " + len + ", array length: " + arrayLength); + } + } + + private Validate() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/ca/spottedleaf/leafprofiler/LProfileGraph.java b/src/main/java/ca/spottedleaf/leafprofiler/LProfileGraph.java new file mode 100644 index 0000000..26c4aa3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/leafprofiler/LProfileGraph.java @@ -0,0 +1,107 @@ +package ca.spottedleaf.leafprofiler; + +import it.unimi.dsi.fastutil.ints.Int2IntMap; +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +public final class LProfileGraph { + + public static final int ROOT_NODE = 0; + // Array idx is the graph node id, where the int->int mapping is a mapping of profile timer id to graph node id + private Int2IntOpenHashMap[] nodes; + private int nodeCount; + + public LProfileGraph() { + final Int2IntOpenHashMap[] nodes = new Int2IntOpenHashMap[16]; + nodes[ROOT_NODE] = new Int2IntOpenHashMap(); + + this.nodes = nodes; + this.nodeCount = 1; + } + + public static record GraphNode(GraphNode parent, int nodeId, int timerId) {} + + public List getDFS() { + final List ret = new ArrayList<>(); + final ArrayDeque queue = new ArrayDeque<>(); + + queue.addFirst(new GraphNode(null, ROOT_NODE, -1)); + final Int2IntOpenHashMap[] nodes = this.nodes; + + GraphNode graphNode; + while ((graphNode = queue.pollFirst()) != null) { + ret.add(graphNode); + + final int parent = graphNode.nodeId; + + final Int2IntOpenHashMap children = nodes[parent]; + + for (final Iterator iterator = children.int2IntEntrySet().fastIterator(); iterator.hasNext();) { + final Int2IntMap.Entry entry = iterator.next(); + queue.addFirst(new GraphNode(graphNode, entry.getIntValue(), entry.getIntKey())); + } + } + + return ret; + } + + private int createNode(final int parent, final int timerId) { + Int2IntOpenHashMap[] nodes = this.nodes; + + final Int2IntOpenHashMap node = nodes[parent]; + + final int newNode = this.nodeCount; + final int prev = node.putIfAbsent(timerId, newNode); + + if (prev != 0) { + // already exists + return prev; + } + + // insert new node + ++this.nodeCount; + + if (newNode >= nodes.length) { + this.nodes = (nodes = Arrays.copyOf(nodes, nodes.length * 2)); + } + + nodes[newNode] = new Int2IntOpenHashMap(); + + return newNode; + } + + public int getNode(final int parent, final int timerId) { + // note: requires parent node to exist + final Int2IntOpenHashMap[] nodes = this.nodes; + + if (parent >= nodes.length) { + return -1; + } + + final int mapping = nodes[parent].get(timerId); + + if (mapping != 0) { + return mapping; + } + + return -1; + } + + public int getOrCreateNode(final int parent, final int timerId) { + // note: requires parent node to exist + final Int2IntOpenHashMap[] nodes = this.nodes; + + final int mapping = nodes[parent].get(timerId); + + if (mapping != 0) { + return mapping; + } + + return this.createNode(parent, timerId); + } +} diff --git a/src/main/java/ca/spottedleaf/leafprofiler/LProfilerRegistry.java b/src/main/java/ca/spottedleaf/leafprofiler/LProfilerRegistry.java new file mode 100644 index 0000000..841a615 --- /dev/null +++ b/src/main/java/ca/spottedleaf/leafprofiler/LProfilerRegistry.java @@ -0,0 +1,79 @@ +package ca.spottedleaf.leafprofiler; + +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; + +public final class LProfilerRegistry { + + // volatile required to ensure correct publishing when resizing + private volatile ProfilerEntry[] typesById = new ProfilerEntry[16]; + private int totalEntries; + private final ConcurrentHashMap nameToEntry = new ConcurrentHashMap<>(); + + public LProfilerRegistry() {} + + public ProfilerEntry getById(final int id) { + final ProfilerEntry[] entries = this.typesById; + + return id < 0 || id >= entries.length ? null : entries[id]; + } + + public ProfilerEntry getByName(final String name) { + return this.nameToEntry.get(name); + } + + public int getOrCreateType(final ProfileType type, final String name) { + ProfilerEntry entry = this.nameToEntry.get(name); + if (entry != null) { + return entry.id; + } + synchronized (this) { + entry = this.nameToEntry.get(name); + if (entry != null) { + return entry.id; + } + return this.createType(type, name); + } + } + + public int getOrCreateTimer(final String name) { + return this.getOrCreateType(ProfileType.TIMER, name); + } + + public int getOrCreateCounter(final String name) { + return this.getOrCreateType(ProfileType.COUNTER, name); + } + + public int createType(final ProfileType type, final String name) { + synchronized (this) { + final int id = this.totalEntries; + + final ProfilerEntry ret = new ProfilerEntry(type, name, id); + + final ProfilerEntry prev = this.nameToEntry.putIfAbsent(name, ret); + + if (prev != null) { + throw new IllegalStateException("Entry already exists: " + prev); + } + + ++this.totalEntries; + + ProfilerEntry[] entries = this.typesById; + + if (id >= entries.length) { + this.typesById = entries = Arrays.copyOf(entries, entries.length * 2); + } + + // should be opaque, but I don't think that matters here. + entries[id] = ret; + + return id; + } + } + + public static enum ProfileType { + COUNTER, TIMER; + } + + public static record ProfilerEntry(ProfileType type, String name, int id) {} +} diff --git a/src/main/java/ca/spottedleaf/leafprofiler/LeafProfiler.java b/src/main/java/ca/spottedleaf/leafprofiler/LeafProfiler.java new file mode 100644 index 0000000..ead96cd --- /dev/null +++ b/src/main/java/ca/spottedleaf/leafprofiler/LeafProfiler.java @@ -0,0 +1,382 @@ +package ca.spottedleaf.leafprofiler; + +import com.mojang.logging.LogUtils; +import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import org.slf4j.Logger; +import java.text.DecimalFormat; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class LeafProfiler { + + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final ThreadLocal THREE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { + return new DecimalFormat("#,##0.000"); + }); + private static final ThreadLocal NO_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { + return new DecimalFormat("#,##0"); + }); + + public final LProfilerRegistry registry; + private final LProfileGraph graph; + + private long[] accumulatedTimers = new long[0]; + private long[] accumulatedCounters = new long[0]; + + private long[] timers = new long[16]; + private long[] counters = new long[16]; + private final IntArrayFIFOQueue callStack = new IntArrayFIFOQueue(); + private int topOfStack = LProfileGraph.ROOT_NODE; + private final LongArrayFIFOQueue timerStack = new LongArrayFIFOQueue(); + private long lastTimerStart = 0L; + + public LeafProfiler(final LProfilerRegistry registry, final LProfileGraph graph) { + this.registry = registry; + this.graph = graph; + } + + private static void add(final long[] dst, final long[] src) { + for (int i = 0; i < src.length; ++i) { + dst[i] += src[i]; + } + } + + public ProfilingData copyCurrent() { + return new ProfilingData( + this.registry, this.graph, this.timers.clone(), this.counters.clone() + ); + } + + public ProfilingData copyAccumulated() { + return new ProfilingData( + this.registry, this.graph, this.accumulatedTimers.clone(), this.accumulatedCounters.clone() + ); + } + + public void accumulate() { + if (this.accumulatedTimers.length != this.timers.length) { + this.accumulatedTimers = Arrays.copyOf(this.accumulatedTimers, this.timers.length); + } + add(this.accumulatedTimers, this.timers); + Arrays.fill(this.timers, 0L); + + if (this.accumulatedCounters.length != this.counters.length) { + this.accumulatedCounters = Arrays.copyOf(this.accumulatedCounters, this.counters.length); + } + add(this.accumulatedCounters, this.counters); + Arrays.fill(this.counters, 0L); + } + + private long[] resizeTimers(final long[] old, final int least) { + return this.timers = Arrays.copyOf(old, Math.max(old.length * 2, least * 2)); + } + + private void incrementTimersDirect(final int nodeId, final long count) { + final long[] timers = this.timers; + if (nodeId >= timers.length) { + this.resizeTimers(timers, nodeId)[nodeId] += count; + } else { + timers[nodeId] += count; + } + } + + private long[] resizeCounters(final long[] old, final int least) { + return this.counters = Arrays.copyOf(old, Math.max(old.length * 2, least * 2)); + } + + private void incrementCountersDirect(final int nodeId, final long count) { + final long[] counters = this.counters; + if (nodeId >= counters.length) { + this.resizeCounters(counters, nodeId)[nodeId] += count; + } else { + counters[nodeId] += count; + } + } + + public void incrementCounter(final int timerId, final long count) { + final int node = this.graph.getOrCreateNode(this.topOfStack, timerId); + this.incrementCountersDirect(node, count); + } + + public void incrementTimer(final int timerId, final long count) { + final int node = this.graph.getOrCreateNode(this.topOfStack, timerId); + this.incrementTimersDirect(node, count); + } + + public void startTimer(final int timerId, final long startTime) { + final long lastTimerStart = this.lastTimerStart; + final LProfileGraph graph = this.graph; + final int parentNode = this.topOfStack; + final IntArrayFIFOQueue callStack = this.callStack; + final LongArrayFIFOQueue timerStack = this.timerStack; + + this.lastTimerStart = startTime; + this.topOfStack = graph.getOrCreateNode(parentNode, timerId); + + callStack.enqueue(parentNode); + timerStack.enqueue(lastTimerStart); + } + + public void stopTimer(final int timerId, final long endTime) { + final long lastStart = this.lastTimerStart; + final int currentNode = this.topOfStack; + final IntArrayFIFOQueue callStack = this.callStack; + final LongArrayFIFOQueue timerStack = this.timerStack; + this.lastTimerStart = timerStack.dequeueLastLong(); + this.topOfStack = callStack.dequeueLastInt(); + + if (currentNode != this.graph.getNode(this.topOfStack, timerId)) { + final LProfilerRegistry.ProfilerEntry timer = this.registry.getById(timerId); + throw new IllegalStateException("Timer " + (timer == null ? "null" : timer.name()) + " did not stop"); + } + + this.incrementTimersDirect(currentNode, endTime - lastStart); + this.incrementCountersDirect(currentNode, 1L); + } + + public void stopLastTimer(final long endTime) { + final long lastStart = this.lastTimerStart; + final int currentNode = this.topOfStack; + final IntArrayFIFOQueue callStack = this.callStack; + final LongArrayFIFOQueue timerStack = this.timerStack; + this.lastTimerStart = timerStack.dequeueLastLong(); + this.topOfStack = callStack.dequeueLastInt(); + + this.incrementTimersDirect(currentNode, endTime - lastStart); + this.incrementCountersDirect(currentNode, 1L); + } + + private static final class ProfileNode { + + public final ProfileNode parent; + public final int nodeId; + public final LProfilerRegistry.ProfilerEntry profiler; + public final long totalTime; + public final long totalCount; + public final List children = new ArrayList<>(); + public long childrenTimingCount; + public int depth = -1; + + private ProfileNode(final ProfileNode parent, final int nodeId, final LProfilerRegistry.ProfilerEntry profiler, + final long totalTime, final long totalCount) { + this.parent = parent; + this.nodeId = nodeId; + this.profiler = profiler; + this.totalTime = totalTime; + this.totalCount = totalCount; + } + } + + + + public static final record ProfilingData( + LProfilerRegistry registry, + LProfileGraph graph, + long[] timers, + long[] counters + ) { + private static final char[][] INDENT_PATTERNS = new char[][] { + "|---".toCharArray(), + "|+++".toCharArray(), + }; + + public List dumpToString() { + final List graphDFS = this.graph.getDFS(); + final Reference2ReferenceOpenHashMap nodeMap = new Reference2ReferenceOpenHashMap<>(); + + final ArrayDeque orderedNodes = new ArrayDeque<>(); + + for (int i = 0, len = graphDFS.size(); i < len; ++i) { + final LProfileGraph.GraphNode graphNode = graphDFS.get(i); + final ProfileNode parent = nodeMap.get(graphNode.parent()); + final int nodeId = graphNode.nodeId(); + + final long totalTime = nodeId >= this.timers.length ? 0L : this.timers[nodeId]; + final long totalCount = nodeId >= this.counters.length ? 0L : this.counters[nodeId]; + final LProfilerRegistry.ProfilerEntry profiler = this.registry.getById(graphNode.timerId()); + + final ProfileNode profileNode = new ProfileNode(parent, nodeId, profiler, totalTime, totalCount); + + if (parent != null) { + parent.childrenTimingCount += totalTime; + parent.children.add(profileNode); + } else if (i != 0) { // i == 0 is root + throw new IllegalStateException("Node " + nodeId + " must have parent"); + } else { + // set up + orderedNodes.add(profileNode); + } + + nodeMap.put(graphNode, profileNode); + } + + final List ret = new ArrayList<>(); + + long totalTime = 0L; + + // totalTime = sum of times for root node's children + for (final ProfileNode node : orderedNodes.peekFirst().children) { + totalTime += node.totalTime; + } + + ProfileNode profileNode; + final StringBuilder builder = new StringBuilder(); + while ((profileNode = orderedNodes.pollFirst()) != null) { + if (profileNode.nodeId != LProfileGraph.ROOT_NODE && profileNode.totalCount == 0L) { + // skip nodes not recorded + continue; + } + + final int depth = profileNode.depth; + profileNode.children.sort((final ProfileNode p1, final ProfileNode p2) -> { + final int typeCompare = p1.profiler.type().compareTo(p2.profiler.type()); + if (typeCompare != 0) { + // first count, then profiler + return typeCompare; + } + + if (p1.profiler.type() == LProfilerRegistry.ProfileType.COUNTER) { + // highest count first + return Long.compare(p2.totalCount, p1.totalCount); + } else { + // highest time first + return Long.compare(p2.totalTime, p1.totalTime); + } + }); + + for (int i = profileNode.children.size() - 1; i >= 0; --i) { + final ProfileNode child = profileNode.children.get(i); + child.depth = depth + 1; + orderedNodes.addFirst(child); + } + + if (profileNode.nodeId == LProfileGraph.ROOT_NODE) { + // don't display root + continue; + } + + final boolean noParent = profileNode.parent == null || profileNode.parent.nodeId == LProfileGraph.ROOT_NODE; + + final long parentTime = noParent ? totalTime : profileNode.parent.totalTime; + final LProfilerRegistry.ProfilerEntry profilerEntry = profileNode.profiler; + + // format: + // For profiler type: + // X% total, Y% parent, self A% total, self B% children, avg X sum Y, Dms raw sum + // For counter type: + // # avg X sum Y + builder.setLength(0); + // prepare indent + final char[] indent = INDENT_PATTERNS[ret.size() % INDENT_PATTERNS.length]; + for (int i = 0; i < depth; ++i) { + builder.append(indent); + } + + switch (profilerEntry.type()) { + case TIMER: { + ret.add( + builder + .append(profilerEntry.name()) + .append(' ') + .append(THREE_DECIMAL_PLACES.get().format(((double)profileNode.totalTime / (double)totalTime) * 100.0)) + .append("% total, ") + .append(THREE_DECIMAL_PLACES.get().format(((double)profileNode.totalTime / (double)parentTime) * 100.0)) + .append("% parent, self ") + .append(THREE_DECIMAL_PLACES.get().format(((double)(profileNode.totalTime - profileNode.childrenTimingCount) / (double)totalTime) * 100.0)) + .append("% total, self ") + .append(THREE_DECIMAL_PLACES.get().format(((double)(profileNode.totalTime - profileNode.childrenTimingCount) / (double)profileNode.totalTime) * 100.0)) + .append("% children, avg ") + .append(THREE_DECIMAL_PLACES.get().format((double)profileNode.totalCount / (double)(noParent ? 1L : profileNode.parent.totalCount))) + .append(" sum ") + .append(NO_DECIMAL_PLACES.get().format(profileNode.totalCount)) + .append(", ") + .append(THREE_DECIMAL_PLACES.get().format((double)profileNode.totalTime / 1.0E6)) + .append("ms raw sum") + .toString() + ); + break; + } + case COUNTER: { + ret.add( + builder + .append('#') + .append(profilerEntry.name()) + .append(" avg ") + .append(THREE_DECIMAL_PLACES.get().format((double)profileNode.totalCount / (double)(noParent ? 1L : profileNode.parent.totalCount))) + .append(" sum ") + .append(NO_DECIMAL_PLACES.get().format(profileNode.totalCount)) + .toString() + ); + break; + } + default: { + throw new IllegalStateException("Unknown type " + profilerEntry.type()); + } + } + } + + return ret; + } + } + + /* + public static void main(final String[] args) throws Throwable { + final Thread timerHack = new Thread("Timer hack thread") { + @Override + public void run() { + for (;;) { + try { + Thread.sleep(Long.MAX_VALUE); + } catch (final InterruptedException ex) { + continue; + } + } + } + }; + timerHack.setDaemon(true); + timerHack.start(); + + final LProfilerRegistry registry = new LProfilerRegistry(); + + final int tickId = registry.createType(LProfilerRegistry.ProfileType.TIMER, "tick"); + final int entityTickId = registry.createType(LProfilerRegistry.ProfileType.TIMER, "entity tick"); + final int getEntitiesId = registry.createType(LProfilerRegistry.ProfileType.COUNTER, "getEntities call"); + final int tileEntityId = registry.createType(LProfilerRegistry.ProfileType.TIMER, "tile entity tick"); + final int creeperEntityId = registry.createType(LProfilerRegistry.ProfileType.TIMER, "creeper entity tick"); + final int furnaceId = registry.createType(LProfilerRegistry.ProfileType.TIMER, "furnace tile entity tick"); + + final LeafProfiler profiler = new LeafProfiler(registry, new LProfileGraph()); + + profiler.startTimer(tickId, System.nanoTime()); + Thread.sleep(10L); + + profiler.startTimer(entityTickId, System.nanoTime()); + Thread.sleep(1L); + + profiler.startTimer(creeperEntityId, System.nanoTime()); + Thread.sleep(15L); + profiler.incrementCounter(getEntitiesId, 50L); + profiler.stopTimer(creeperEntityId, System.nanoTime()); + + profiler.stopTimer(entityTickId, System.nanoTime()); + + profiler.startTimer(tileEntityId, System.nanoTime()); + Thread.sleep(1L); + + profiler.startTimer(furnaceId, System.nanoTime()); + Thread.sleep(20L); + profiler.stopTimer(furnaceId, System.nanoTime()); + + profiler.stopTimer(tileEntityId, System.nanoTime()); + + profiler.stopTimer(tickId, System.nanoTime()); + + System.out.println("Done."); + } + */ +} diff --git a/src/main/java/ca/spottedleaf/leafprofiler/TickAccumulator.java b/src/main/java/ca/spottedleaf/leafprofiler/TickAccumulator.java new file mode 100644 index 0000000..8d6ed66 --- /dev/null +++ b/src/main/java/ca/spottedleaf/leafprofiler/TickAccumulator.java @@ -0,0 +1,341 @@ +package ca.spottedleaf.leafprofiler; + +import ca.spottedleaf.concurrentutil.util.TimeUtil; +import ca.spottedleaf.moonrise.common.list.SortedList; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class TickAccumulator { + + private final long interval; // ns + + private final ArrayDeque timeData = new ArrayDeque<>(); + private final SortedList sortedTimeData = new SortedList<>((final TickTime t1, final TickTime t2) -> { + return Long.compare(t1.tickLength(), t2.tickLength()); + }); + + public TickAccumulator(final long intervalNS) { + this.interval = intervalNS; + } + + public int size() { + return this.sortedTimeData.size(); + } + + public int addDataFrom(final TickTime time) { + final long start = time.tickStart(); + + TickTime first; + while ((first = this.timeData.peekFirst()) != null) { + // only remove data completely out of window + if ((start - first.tickEnd()) <= this.interval) { + break; + } + this.timeData.pollFirst(); + this.sortedTimeData.remove(first); + } + + this.timeData.add(time); + return this.sortedTimeData.add(time); + } + + // fromIndex inclusive, toIndex exclusive + // will throw if arr.length == 0 + private static double median(final long[] arr, final int fromIndex, final int toIndex) { + final int len = toIndex - fromIndex; + final int middle = fromIndex + (len >>> 1); + if ((len & 1) == 0) { + // even, average the two middle points + return (double)(arr[middle - 1] + arr[middle]) / 2.0; + } else { + // odd, just grab the middle + return (double)arr[middle]; + } + } + + // will throw if arr.length == 0 + private static SegmentData computeSegmentData(final long[] arr, final int fromIndex, final int toIndex, + final boolean inverse) { + final int len = toIndex - fromIndex; + long sum = 0L; + final double median = median(arr, fromIndex, toIndex); + long min = arr[0]; + long max = arr[0]; + + for (int i = fromIndex; i < toIndex; ++i) { + final long val = arr[i]; + sum += val; + if (val < min) { + min = val; + } + if (val > max) { + max = val; + } + } + + if (inverse) { + // for positive a,b we have that a >= b if and only if 1/a <= 1/b + return new SegmentData( + len, + (double)len / ((double)sum / 1.0E9), + 1.0E9 / median, + 1.0E9 / (double)max, + 1.0E9 / (double)min + ); + } else { + return new SegmentData( + len, + (double)sum / (double)len, + median, + (double)min, + (double)max + ); + } + } + + private static SegmentedAverage computeSegmentedAverage(final long[] data, final int allStart, final int allEnd, + final int percent99BestStart, final int percent99BestEnd, + final int percent95BestStart, final int percent95BestEnd, + final int percent1WorstStart, final int percent1WorstEnd, + final int percent5WorstStart, final int percent5WorstEnd, + final boolean inverse) { + return new SegmentedAverage( + computeSegmentData(data, allStart, allEnd, inverse), + computeSegmentData(data, percent99BestStart, percent99BestEnd, inverse), + computeSegmentData(data, percent95BestStart, percent95BestEnd, inverse), + computeSegmentData(data, percent1WorstStart, percent1WorstEnd, inverse), + computeSegmentData(data, percent5WorstStart, percent5WorstEnd, inverse) + ); + } + + private static record TickInformation( + long differenceFromLastTick, + long tickTime, + long tickTimeCPU + ) {} + + // rets null if there is no data + public TickReportData generateTickReport(final TickTime inProgress, final long endTime) { + if (this.timeData.isEmpty() && inProgress == null) { + return null; + } + + final List allData = new ArrayList<>(this.timeData); + if (inProgress != null) { + allData.add(inProgress); + } + + final long intervalStart = allData.get(0).tickStart(); + final long intervalEnd = allData.get(allData.size() - 1).tickEnd(); + + // to make utilisation accurate, we need to take the total time used over the last interval period - + // this means if a tick start before the measurement interval, but ends within the interval, then we + // only consider the time it spent ticking inside the interval + long totalTimeOverInterval = 0L; + long measureStart = endTime - this.interval; + + for (int i = 0, len = allData.size(); i < len; ++i) { + final TickTime time = allData.get(i); + if (TimeUtil.compareTimes(time.tickStart(), measureStart) < 0) { + final long diff = time.tickEnd() - measureStart; + if (diff > 0L) { + totalTimeOverInterval += diff; + } // else: the time is entirely out of interval + } else { + totalTimeOverInterval += time.tickLength(); + } + } + + // we only care about ticks, but because of inbetween tick task execution + // there will be data in allData that isn't ticks. But, that data cannot + // be ignored since it contributes to utilisation. + // So, we will "compact" the data by merging any inbetween tick times + // the next tick. + // If there is no "next tick", then we will create one. + final List collapsedData = new ArrayList<>(); + for (int i = 0, len = allData.size(); i < len; ++i) { + final List toCollapse = new ArrayList<>(); + TickTime lastTick = null; + for (;i < len; ++i) { + final TickTime time = allData.get(i); + if (false) { + toCollapse.add(time); + continue; + } + lastTick = time; + break; + } + + if (toCollapse.isEmpty()) { + // nothing to collapse + final TickTime last = allData.get(i); + collapsedData.add( + new TickInformation( + last.differenceFromLastTick(0L), + last.tickLength(), + last.supportCPUTime() ? last.tickCpuTime() : 0L + ) + ); + } else { + long totalTickTime = 0L; + long totalCpuTime = 0L; + for (int k = 0, len2 = collapsedData.size(); k < len2; ++k) { + final TickTime time = toCollapse.get(k); + totalTickTime += time.tickLength(); + totalCpuTime += time.supportCPUTime() ? time.tickCpuTime() : 0L; + } + if (i < len) { + // we know there is a tick to collapse into + final TickTime last = allData.get(i); + collapsedData.add( + new TickInformation( + last.differenceFromLastTick(0L), + last.tickLength() + totalTickTime, + (last.supportCPUTime() ? last.tickCpuTime() : 0L) + totalCpuTime + ) + ); + } else { + // we do not have a tick to collapse into, so we must make one up + // we will assume that the tick is "starting now" and ongoing + + // compute difference between imaginary tick and last tick + final long differenceBetweenTicks; + if (lastTick != null) { + // we have a last tick, use it + differenceBetweenTicks = lastTick.tickStart(); + } else { + // we don't have a last tick, so we must make one up that makes sense + // if the current interval exceeds the max tick time, then use it + + // Otherwise use the interval length. + // This is how differenceFromLastTick() works on TickTime when there is no previous interval. + differenceBetweenTicks = Math.max( + 0L, totalTickTime + ); + } + + collapsedData.add( + new TickInformation( + differenceBetweenTicks, + totalTickTime, + totalCpuTime + ) + ); + } + } + } + + + final int collectedTicks = collapsedData.size(); + final long[] tickStartToStartDifferences = new long[collectedTicks]; + final long[] timePerTickDataRaw = new long[collectedTicks]; + final long[] missingCPUTimeDataRaw = new long[collectedTicks]; + + long totalTimeTicking = 0L; + + int i = 0; + for (final TickInformation time : collapsedData) { + tickStartToStartDifferences[i] = time.differenceFromLastTick(); + final long timePerTick = timePerTickDataRaw[i] = time.tickTime(); + missingCPUTimeDataRaw[i] = Math.max(0L, timePerTick - time.tickTimeCPU()); + + ++i; + + totalTimeTicking += timePerTick; + } + + Arrays.sort(tickStartToStartDifferences); + Arrays.sort(timePerTickDataRaw); + Arrays.sort(missingCPUTimeDataRaw); + + // Note: computeSegmentData cannot take start == end + final int allStart = 0; + final int allEnd = collectedTicks; + final int percent95BestStart = 0; + final int percent95BestEnd = collectedTicks == 1 ? 1 : (int)(0.95 * collectedTicks); + final int percent99BestStart = 0; + // (int)(0.99 * collectedTicks) == 0 if collectedTicks = 1, so we need to use 1 to avoid start == end + final int percent99BestEnd = collectedTicks == 1 ? 1 : (int)(0.99 * collectedTicks); + final int percent1WorstStart = (int)(0.99 * collectedTicks); + final int percent1WorstEnd = collectedTicks; + final int percent5WorstStart = (int)(0.95 * collectedTicks); + final int percent5WorstEnd = collectedTicks; + + final SegmentedAverage tpsData = computeSegmentedAverage( + tickStartToStartDifferences, + allStart, allEnd, + percent99BestStart, percent99BestEnd, + percent95BestStart, percent95BestEnd, + percent1WorstStart, percent1WorstEnd, + percent5WorstStart, percent5WorstEnd, + true + ); + + final SegmentedAverage timePerTickData = computeSegmentedAverage( + timePerTickDataRaw, + allStart, allEnd, + percent99BestStart, percent99BestEnd, + percent95BestStart, percent95BestEnd, + percent1WorstStart, percent1WorstEnd, + percent5WorstStart, percent5WorstEnd, + false + ); + + final SegmentedAverage missingCPUTimeData = computeSegmentedAverage( + missingCPUTimeDataRaw, + allStart, allEnd, + percent99BestStart, percent99BestEnd, + percent95BestStart, percent95BestEnd, + percent1WorstStart, percent1WorstEnd, + percent5WorstStart, percent5WorstEnd, + false + ); + + final double utilisation = (double)totalTimeOverInterval / (double)this.interval; + + return new TickReportData( + collectedTicks, + intervalStart, + intervalEnd, + totalTimeTicking, + utilisation, + + tpsData, + timePerTickData, + missingCPUTimeData + ); + } + + public static final record TickReportData( + int collectedTicks, + long collectedTickIntervalStart, + long collectedTickIntervalEnd, + long totalTimeTicking, + double utilisation, + + SegmentedAverage tpsData, + // in ns + SegmentedAverage timePerTickData, + // in ns + SegmentedAverage missingCPUTimeData + ) {} + + public static final record SegmentedAverage( + SegmentData segmentAll, + SegmentData segment99PercentBest, + SegmentData segment95PercentBest, + SegmentData segment5PercentWorst, + SegmentData segment1PercentWorst + ) {} + + public static final record SegmentData( + int count, + double average, + double median, + double least, + double greatest + ) {} +} diff --git a/src/main/java/ca/spottedleaf/leafprofiler/TickTime.java b/src/main/java/ca/spottedleaf/leafprofiler/TickTime.java new file mode 100644 index 0000000..aa90f3b --- /dev/null +++ b/src/main/java/ca/spottedleaf/leafprofiler/TickTime.java @@ -0,0 +1,67 @@ +package ca.spottedleaf.leafprofiler; + +public final record TickTime( + long previousTickStart, + long scheduledTickStart, + long tickStart, + long tickStartCPU, + long tickEnd, + long tickEndCPU, + boolean supportCPUTime +) { + public static final long CPU_TIME_UNSUPPORTED = Long.MIN_VALUE; + public static final long DEADLINE_NOT_SET = Long.MIN_VALUE; + + /** + * The difference between the start tick time and the scheduled start tick time. This value is + * < 0 if the tick started before the scheduled tick time. + */ + public final long startOvershoot() { + return this.tickStart - this.scheduledTickStart; + } + + /** + * The difference from the end tick time and the start tick time. Always >= 0 (unless nanoTime is just wrong). + */ + public final long tickLength() { + return this.tickEnd - this.tickStart; + } + + /** + * The total CPU time from the start tick time to the end tick time. Generally should be equal to the tickLength, + * unless there is CPU starvation or the tick thread was blocked by I/O or other tasks. Returns {@link #CPU_TIME_UNSUPPORTED} + * if CPU time measurement is not supported. + */ + public final long tickCpuTime() { + if (!this.supportCPUTime()) { + return CPU_TIME_UNSUPPORTED; + } + return this.tickEndCPU - this.tickStartCPU; + } + + /** + * The difference in time from the start of the last tick to the start of the current tick. If there is no + * last tick, then this value is max(defaultTime, tickLength). + */ + public final long differenceFromLastTick(final long defaultTime) { + if (this.hasLastTick()) { + return this.tickStart - this.previousTickStart; + } + return Math.max(defaultTime, this.tickLength()); + } + + /** + * Returns whether there was a tick that occurred before this one. + */ + public final boolean hasLastTick() { + return this.previousTickStart != DEADLINE_NOT_SET; + } + + /* + * Remember, this is the expected behavior of the following: + * + * MSPT: Time per tick. This does not include overshoot time, just the tickLength(). + * + * TPS: The number of ticks per second. It should be ticks / (sum of differenceFromLastTick). + */ +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/leafprofiler/client/ClientProfilerInstance.java b/src/main/java/ca/spottedleaf/leafprofiler/client/ClientProfilerInstance.java new file mode 100644 index 0000000..a0ebd18 --- /dev/null +++ b/src/main/java/ca/spottedleaf/leafprofiler/client/ClientProfilerInstance.java @@ -0,0 +1,202 @@ +package ca.spottedleaf.leafprofiler.client; + +import ca.spottedleaf.leafprofiler.LProfileGraph; +import ca.spottedleaf.leafprofiler.LProfilerRegistry; +import ca.spottedleaf.leafprofiler.LeafProfiler; +import ca.spottedleaf.leafprofiler.TickAccumulator; +import ca.spottedleaf.leafprofiler.TickTime; +import com.mojang.logging.LogUtils; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.util.profiling.metrics.MetricCategory; +import org.slf4j.Logger; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public final class ClientProfilerInstance implements ProfilerFiller { + + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean(); + private static final boolean MEASURE_CPU_TIME; + static { + MEASURE_CPU_TIME = THREAD_MX_BEAN.isThreadCpuTimeSupported(); + if (MEASURE_CPU_TIME) { + THREAD_MX_BEAN.setThreadCpuTimeEnabled(true); + } else { + LOGGER.warn("TickRegionScheduler CPU time measurement is not available"); + } + } + + private final LProfilerRegistry registry = new LProfilerRegistry(); + private final TickAccumulator tickAccumulator = new TickAccumulator(TimeUnit.SECONDS.toNanos(1L)); + + public final int clientFrame = this.registry.createType(LProfilerRegistry.ProfileType.TIMER, "Client Frame"); + public final int clientTick = this.registry.createType(LProfilerRegistry.ProfileType.TIMER, "Client Tick"); + + private long previousTickStart = TickTime.DEADLINE_NOT_SET; + private long tickStart = TickTime.DEADLINE_NOT_SET; + private long tickStartCPU = TickTime.DEADLINE_NOT_SET; + + private LeafProfiler delayedFrameProfiler; + private LeafProfiler frameProfiler; + + private static final double LARGE_TICK_THRESHOLD = 1.0 - 0.05; + + private long tick; + + private final List largeTicks = new ArrayList<>(); + + private static record LargeTick(long tickNum, LeafProfiler.ProfilingData profile) {} + + public void startProfiler() { + this.delayedFrameProfiler = new LeafProfiler(this.registry, new LProfileGraph()); + } + + public void stopProfiler() { + this.delayedFrameProfiler = null; + } + + @Override + public void startTick() { + final long time = System.nanoTime(); + final long cpuTime = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; + this.previousTickStart = this.tickStart; + this.tickStart = time; + this.tickStartCPU = cpuTime; + + this.frameProfiler = this.delayedFrameProfiler; + if (this.frameProfiler != null) { + this.frameProfiler.startTimer(this.clientFrame, time); + } + } + + @Override + public void endTick() { + final long cpuTime = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; + final long time = System.nanoTime(); + + final TickTime tickTime = new TickTime( + this.previousTickStart, this.tickStart, this.tickStart, this.tickStartCPU, time, cpuTime, MEASURE_CPU_TIME + ); + + final int index = this.tickAccumulator.addDataFrom(tickTime); + final int count = this.tickAccumulator.size(); + + if (this.frameProfiler != null) { + this.frameProfiler.stopTimer(this.clientFrame, time); + + if (count == 1 || (double)index/(double)(count - 1) >= LARGE_TICK_THRESHOLD) { + this.largeTicks.add(new LargeTick(this.tick, this.frameProfiler.copyCurrent())); + } + + this.frameProfiler.accumulate(); + } + this.frameProfiler = null; + ++this.tick; + } + + public void startRealClientTick() { + if (this.frameProfiler != null) { + this.frameProfiler.startTimer(this.clientTick, System.nanoTime()); + } + } + + public void endRealClientTick() { + if (this.frameProfiler != null) { + this.frameProfiler.stopTimer(this.clientTick, System.nanoTime()); + } + } + + @Override + public void push(final String string) { + final LeafProfiler frameProfiler = this.frameProfiler; + if (frameProfiler == null) { + return; + } + + final long time = System.nanoTime(); + final int timerId = frameProfiler.registry.getOrCreateTimer(string); + + frameProfiler.startTimer(timerId, time); + } + + @Override + public void push(final Supplier supplier) { + final LeafProfiler frameProfiler = this.frameProfiler; + if (frameProfiler == null) { + return; + } + + final long time = System.nanoTime(); + final int timerId = frameProfiler.registry.getOrCreateTimer(supplier.get()); + + frameProfiler.startTimer(timerId, time); + } + + @Override + public void pop() { + final LeafProfiler frameProfiler = this.frameProfiler; + if (frameProfiler == null) { + return; + } + + final long time = System.nanoTime(); + + frameProfiler.stopLastTimer(time); + } + + @Override + public void popPush(final String string) { + this.pop(); + this.push(string); + } + + @Override + public void popPush(final Supplier supplier) { + this.pop(); + this.push(supplier); + } + + @Override + public void markForCharting(final MetricCategory metricCategory) { + // what the fuck is this supposed to do? + } + + @Override + public void incrementCounter(final String string) { + this.incrementCounter(string, 1); + } + + @Override + public void incrementCounter(final String string, final int i) { + final LeafProfiler frameProfiler = this.frameProfiler; + if (frameProfiler == null) { + return; + } + + final int timerId = frameProfiler.registry.getOrCreateCounter(string); + + frameProfiler.incrementCounter(timerId, (long)i); + } + + @Override + public void incrementCounter(final Supplier supplier) { + this.incrementCounter(supplier, 1); + } + + @Override + public void incrementCounter(final Supplier supplier, final int i) { + final LeafProfiler frameProfiler = this.frameProfiler; + if (frameProfiler == null) { + return; + } + + final int timerId = frameProfiler.registry.getOrCreateCounter(supplier.get()); + + frameProfiler.incrementCounter(timerId, (long)i); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java new file mode 100644 index 0000000..136ce6a --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java @@ -0,0 +1,125 @@ +package ca.spottedleaf.moonrise.common.list; + +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.shorts.Short2LongOpenHashMap; +import java.util.Arrays; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.GlobalPalette; + +public final class IBlockDataList { + + static final GlobalPalette GLOBAL_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY); + + // map of location -> (index | (location << 16) | (palette id << 32)) + private final Short2LongOpenHashMap map = new Short2LongOpenHashMap(2, 0.8f); + { + this.map.defaultReturnValue(Long.MAX_VALUE); + } + + private static final long[] EMPTY_LIST = new long[0]; + + private long[] byIndex = EMPTY_LIST; + private int size; + + public static int getLocationKey(final int x, final int y, final int z) { + return (x & 15) | (((z & 15) << 4)) | ((y & 255) << (4 + 4)); + } + + public static BlockState getBlockDataFromRaw(final long raw) { + return GLOBAL_PALETTE.valueFor((int)(raw >>> 32)); + } + + public static int getIndexFromRaw(final long raw) { + return (int)(raw & 0xFFFF); + } + + public static int getLocationFromRaw(final long raw) { + return (int)((raw >>> 16) & 0xFFFF); + } + + public static long getRawFromValues(final int index, final int location, final BlockState data) { + return (long)index | ((long)location << 16) | (((long)GLOBAL_PALETTE.idFor(data)) << 32); + } + + public static long setIndexRawValues(final long value, final int index) { + return value & ~(0xFFFF) | (index); + } + + public long add(final int x, final int y, final int z, final BlockState data) { + return this.add(getLocationKey(x, y, z), data); + } + + public long add(final int location, final BlockState data) { + final long curr = this.map.get((short)location); + + if (curr == Long.MAX_VALUE) { + final int index = this.size++; + final long raw = getRawFromValues(index, location, data); + this.map.put((short)location, raw); + + if (index >= this.byIndex.length) { + this.byIndex = Arrays.copyOf(this.byIndex, (int)Math.max(4L, this.byIndex.length * 2L)); + } + + this.byIndex[index] = raw; + return raw; + } else { + final int index = getIndexFromRaw(curr); + final long raw = this.byIndex[index] = getRawFromValues(index, location, data); + + this.map.put((short)location, raw); + + return raw; + } + } + + public long remove(final int x, final int y, final int z) { + return this.remove(getLocationKey(x, y, z)); + } + + public long remove(final int location) { + final long ret = this.map.remove((short)location); + final int index = getIndexFromRaw(ret); + if (ret == Long.MAX_VALUE) { + return ret; + } + + // move the entry at the end to this index + final int endIndex = --this.size; + final long end = this.byIndex[endIndex]; + if (index != endIndex) { + // not empty after this call + this.map.put((short)getLocationFromRaw(end), setIndexRawValues(end, index)); + } + this.byIndex[index] = end; + this.byIndex[endIndex] = 0L; + + return ret; + } + + public int size() { + return this.size; + } + + public long getRaw(final int index) { + return this.byIndex[index]; + } + + public int getLocation(final int index) { + return getLocationFromRaw(this.getRaw(index)); + } + + public BlockState getData(final int index) { + return getBlockDataFromRaw(this.getRaw(index)); + } + + public void clear() { + this.size = 0; + this.map.clear(); + } + + public LongIterator getRawIterator() { + return this.map.values().iterator(); + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java new file mode 100644 index 0000000..fa854fe --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java @@ -0,0 +1,312 @@ +package ca.spottedleaf.moonrise.common.list; + +import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; +import java.util.Arrays; +import java.util.NoSuchElementException; + +public final class IteratorSafeOrderedReferenceSet { + + public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0; + + protected final Reference2IntLinkedOpenHashMap indexMap; + protected int firstInvalidIndex = -1; + + /* list impl */ + protected E[] listElements; + protected int listSize; + + protected final double maxFragFactor; + + protected int iteratorCount; + + public IteratorSafeOrderedReferenceSet() { + this(16, 0.75f, 16, 0.2); + } + + public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity, + final double maxFragFactor) { + this.indexMap = new Reference2IntLinkedOpenHashMap<>(setCapacity, setLoadFactor); + this.indexMap.defaultReturnValue(-1); + this.maxFragFactor = maxFragFactor; + this.listElements = (E[])new Object[arrayCapacity]; + } + + /* + public void check() { + int iterated = 0; + ReferenceOpenHashSet check = new ReferenceOpenHashSet<>(); + if (this.listElements != null) { + for (int i = 0; i < this.listSize; ++i) { + Object obj = this.listElements[i]; + if (obj != null) { + iterated++; + if (!check.add((E)obj)) { + throw new IllegalStateException("contains duplicate"); + } + if (!this.contains((E)obj)) { + throw new IllegalStateException("desync"); + } + } + } + } + + if (iterated != this.size()) { + throw new IllegalStateException("Size is mismatched! Got " + iterated + ", expected " + this.size()); + } + + check.clear(); + iterated = 0; + for (final java.util.Iterator iterator = this.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + final E element = iterator.next(); + iterated++; + if (!check.add(element)) { + throw new IllegalStateException("contains duplicate (iterator is wrong)"); + } + if (!this.contains(element)) { + throw new IllegalStateException("desync (iterator is wrong)"); + } + } + + if (iterated != this.size()) { + throw new IllegalStateException("Size is mismatched! (iterator is wrong) Got " + iterated + ", expected " + this.size()); + } + } + */ + + protected final double getFragFactor() { + return 1.0 - ((double)this.indexMap.size() / (double)this.listSize); + } + + public int createRawIterator() { + ++this.iteratorCount; + if (this.indexMap.isEmpty()) { + return -1; + } else { + return this.firstInvalidIndex == 0 ? this.indexMap.getInt(this.indexMap.firstKey()) : 0; + } + } + + public int advanceRawIterator(final int index) { + final E[] elements = this.listElements; + int ret = index + 1; + for (int len = this.listSize; ret < len; ++ret) { + if (elements[ret] != null) { + return ret; + } + } + + return -1; + } + + public void finishRawIterator() { + if (--this.iteratorCount == 0) { + if (this.getFragFactor() >= this.maxFragFactor) { + this.defrag(); + } + } + } + + public boolean remove(final E element) { + final int index = this.indexMap.removeInt(element); + if (index >= 0) { + if (this.firstInvalidIndex < 0 || index < this.firstInvalidIndex) { + this.firstInvalidIndex = index; + } + if (this.listElements[index] != element) { + throw new IllegalStateException(); + } + this.listElements[index] = null; + if (this.iteratorCount == 0 && this.getFragFactor() >= this.maxFragFactor) { + this.defrag(); + } + //this.check(); + return true; + } + return false; + } + + public boolean contains(final E element) { + return this.indexMap.containsKey(element); + } + + public boolean add(final E element) { + final int listSize = this.listSize; + + final int previous = this.indexMap.putIfAbsent(element, listSize); + if (previous != -1) { + return false; + } + + if (listSize >= this.listElements.length) { + this.listElements = Arrays.copyOf(this.listElements, listSize * 2); + } + this.listElements[listSize] = element; + this.listSize = listSize + 1; + + //this.check(); + return true; + } + + protected void defrag() { + if (this.firstInvalidIndex < 0) { + return; // nothing to do + } + + if (this.indexMap.isEmpty()) { + Arrays.fill(this.listElements, 0, this.listSize, null); + this.listSize = 0; + this.firstInvalidIndex = -1; + //this.check(); + return; + } + + final E[] backingArray = this.listElements; + + int lastValidIndex; + java.util.Iterator> iterator; + + if (this.firstInvalidIndex == 0) { + iterator = this.indexMap.reference2IntEntrySet().fastIterator(); + lastValidIndex = 0; + } else { + lastValidIndex = this.firstInvalidIndex; + final E key = backingArray[lastValidIndex - 1]; + iterator = this.indexMap.reference2IntEntrySet().fastIterator(new Reference2IntMap.Entry() { + @Override + public int getIntValue() { + throw new UnsupportedOperationException(); + } + + @Override + public int setValue(int i) { + throw new UnsupportedOperationException(); + } + + @Override + public E getKey() { + return key; + } + }); + } + + while (iterator.hasNext()) { + final Reference2IntMap.Entry entry = iterator.next(); + + final int newIndex = lastValidIndex++; + backingArray[newIndex] = entry.getKey(); + entry.setValue(newIndex); + } + + // cleanup end + Arrays.fill(backingArray, lastValidIndex, this.listSize, null); + this.listSize = lastValidIndex; + this.firstInvalidIndex = -1; + //this.check(); + } + + public E rawGet(final int index) { + return this.listElements[index]; + } + + public int size() { + // always returns the correct amount - listSize can be different + return this.indexMap.size(); + } + + public IteratorSafeOrderedReferenceSet.Iterator iterator() { + return this.iterator(0); + } + + public IteratorSafeOrderedReferenceSet.Iterator iterator(final int flags) { + ++this.iteratorCount; + return new BaseIterator<>(this, true, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize); + } + + public java.util.Iterator unsafeIterator() { + return this.unsafeIterator(0); + } + public java.util.Iterator unsafeIterator(final int flags) { + return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize); + } + + public static interface Iterator extends java.util.Iterator { + + public void finishedIterating(); + + } + + protected static final class BaseIterator implements IteratorSafeOrderedReferenceSet.Iterator { + + protected final IteratorSafeOrderedReferenceSet set; + protected final boolean canFinish; + protected final int maxIndex; + protected int nextIndex; + protected E pendingValue; + protected boolean finished; + protected E lastReturned; + + protected BaseIterator(final IteratorSafeOrderedReferenceSet set, final boolean canFinish, final int maxIndex) { + this.set = set; + this.canFinish = canFinish; + this.maxIndex = maxIndex; + } + + @Override + public boolean hasNext() { + if (this.finished) { + return false; + } + if (this.pendingValue != null) { + return true; + } + + final E[] elements = this.set.listElements; + int index, len; + for (index = this.nextIndex, len = Math.min(this.maxIndex, this.set.listSize); index < len; ++index) { + final E element = elements[index]; + if (element != null) { + this.pendingValue = element; + this.nextIndex = index + 1; + return true; + } + } + + this.nextIndex = index; + return false; + } + + @Override + public E next() { + if (!this.hasNext()) { + throw new NoSuchElementException(); + } + final E ret = this.pendingValue; + + this.pendingValue = null; + this.lastReturned = ret; + + return ret; + } + + @Override + public void remove() { + final E lastReturned = this.lastReturned; + if (lastReturned == null) { + throw new IllegalStateException(); + } + this.lastReturned = null; + this.set.remove(lastReturned); + } + + @Override + public void finishedIterating() { + if (this.finished || !this.canFinish) { + throw new IllegalStateException(); + } + this.lastReturned = null; + this.finished = true; + this.set.finishRawIterator(); + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java new file mode 100644 index 0000000..7aed734 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java @@ -0,0 +1,122 @@ +package ca.spottedleaf.moonrise.common.list; + +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +public final class ReferenceList implements Iterable { + + protected final Reference2IntOpenHashMap referenceToIndex = new Reference2IntOpenHashMap<>(2, 0.8f); + { + this.referenceToIndex.defaultReturnValue(Integer.MIN_VALUE); + } + + protected static final Object[] EMPTY_LIST = new Object[0]; + + protected Object[] references = EMPTY_LIST; + protected int count; + + public int size() { + return this.count; + } + + public boolean contains(final E obj) { + return this.referenceToIndex.containsKey(obj); + } + + public boolean remove(final E obj) { + final int index = this.referenceToIndex.removeInt(obj); + if (index == Integer.MIN_VALUE) { + return false; + } + + // move the object at the end to this index + final int endIndex = --this.count; + final E end = (E)this.references[endIndex]; + if (index != endIndex) { + // not empty after this call + this.referenceToIndex.put(end, index); // update index + } + this.references[index] = end; + this.references[endIndex] = null; + + return true; + } + + public boolean add(final E obj) { + final int count = this.count; + final int currIndex = this.referenceToIndex.putIfAbsent(obj, count); + + if (currIndex != Integer.MIN_VALUE) { + return false; // already in this list + } + + Object[] list = this.references; + + if (list.length == count) { + // resize required + list = this.references = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative + } + + list[count] = obj; + this.count = count + 1; + + return true; + } + + public E getChecked(final int index) { + if (index < 0 || index >= this.count) { + throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count); + } + return (E)this.references[index]; + } + + public E getUnchecked(final int index) { + return (E)this.references[index]; + } + + public Object[] getRawData() { + return this.references; + } + + public void clear() { + this.referenceToIndex.clear(); + Arrays.fill(this.references, 0, this.count, null); + this.count = 0; + } + + @Override + public Iterator iterator() { + return new Iterator<>() { + private E lastRet; + private int current; + + @Override + public boolean hasNext() { + return this.current < ReferenceList.this.count; + } + + @Override + public E next() { + if (this.current >= ReferenceList.this.count) { + throw new NoSuchElementException(); + } + return this.lastRet = (E)ReferenceList.this.references[this.current++]; + } + + @Override + public void remove() { + final E lastRet = this.lastRet; + + if (lastRet == null) { + throw new IllegalStateException(); + } + this.lastRet = null; + + ReferenceList.this.remove(lastRet); + --this.current; + } + }; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java new file mode 100644 index 0000000..8e386b3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java @@ -0,0 +1,112 @@ +package ca.spottedleaf.moonrise.common.list; + +import java.util.Arrays; +import java.util.Comparator; + +public final class SortedList { + + protected static final Object[] EMPTY_LIST = new Object[0]; + + protected Comparator comparator; + protected Object[] elements; + protected int count; + + public SortedList(final Comparator comparator) { + this.elements = EMPTY_LIST; + this.comparator = comparator; + } + + // start, end are inclusive + private static int insertIdx(final E[] elements, final E element, final Comparator comparator, + int start, int end) { + while (start <= end) { + final int middle = (start + end) >>> 1; + + final E middleVal = elements[middle]; + + final int cmp = comparator.compare(element, middleVal); + + if (cmp < 0) { + end = middle - 1; + } else { + start = middle + 1; + } + } + + return start; + } + + public int size() { + return this.count; + } + + public boolean isEmpty() { + return this.count == 0; + } + + public int add(final E element) { + E[] elements = (E[])this.elements; + final int count = this.count; + this.count = count + 1; + final Comparator comparator = this.comparator; + + final int idx = insertIdx(elements, element, comparator, 0, count - 1); + + if (count >= elements.length) { + // copy and insert at the same time + if (idx == count) { + this.elements = elements = Arrays.copyOf(elements, (int)Math.max(4L, count * 2L)); // overflow results in negative + elements[count] = element; + return idx; + } else { + final Object[] newElements = (E[])new Object[(int)Math.max(4L, count * 2L)]; // overflow results in negative + System.arraycopy(elements, 0, newElements, 0, idx); + newElements[idx] = element; + System.arraycopy(elements, idx, newElements, idx + 1, count - idx); + this.elements = newElements; + return idx; + } + } else { + if (idx == count) { + // no copy needed + elements[idx] = element; + return idx; + } else { + // shift elements down + System.arraycopy(elements, idx, elements, idx + 1, count - idx); + elements[idx] = element; + return idx; + } + } + } + + public E get(final int idx) { + if (idx < 0 || idx >= this.count) { + throw new IndexOutOfBoundsException(idx); + } + return (E)this.elements[idx]; + } + + + public E remove(final E element) { + E[] elements = (E[])this.elements; + final int count = this.count; + final Comparator comparator = this.comparator; + + final int idx = Arrays.binarySearch(elements, 0, count, element, comparator); + if (idx < 0) { + return null; + } + + final int last = this.count - 1; + this.count = last; + + final E ret = elements[idx]; + + System.arraycopy(elements, idx + 1, elements, idx, last - idx); + + elements[last] = null; + + return ret; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java new file mode 100644 index 0000000..62caf61 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java @@ -0,0 +1,77 @@ +package ca.spottedleaf.moonrise.common.map; + +import it.unimi.dsi.fastutil.ints.Int2IntFunction; + +import java.util.Arrays; + +public class Int2IntArraySortedMap { + + protected int[] key; + protected int[] val; + protected int size; + + public Int2IntArraySortedMap() { + this.key = new int[8]; + this.val = new int[8]; + } + + public int put(final int key, final int value) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index >= 0) { + final int current = this.val[index]; + this.val[index] = value; + return current; + } + final int insert = -(index + 1); + // shift entries down + if (this.size >= this.val.length) { + this.key = Arrays.copyOf(this.key, this.key.length * 2); + this.val = Arrays.copyOf(this.val, this.val.length * 2); + } + System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); + System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); + ++this.size; + + this.key[insert] = key; + this.val[insert] = value; + + return 0; + } + + public int computeIfAbsent(final int key, final Int2IntFunction producer) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index >= 0) { + return this.val[index]; + } + final int insert = -(index + 1); + // shift entries down + if (this.size >= this.val.length) { + this.key = Arrays.copyOf(this.key, this.key.length * 2); + this.val = Arrays.copyOf(this.val, this.val.length * 2); + } + System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); + System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); + ++this.size; + + this.key[insert] = key; + + return this.val[insert] = producer.apply(key); + } + + public int get(final int key) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index < 0) { + return 0; + } + return this.val[index]; + } + + public int getFloor(final int key) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index < 0) { + final int insert = -(index + 1) - 1; + return insert < 0 ? 0 : this.val[insert]; + } + return this.val[index]; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java new file mode 100644 index 0000000..fea9e8b --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java @@ -0,0 +1,74 @@ +package ca.spottedleaf.moonrise.common.map; + +import java.util.Arrays; +import java.util.function.IntFunction; + +public class Int2ObjectArraySortedMap { + + protected int[] key; + protected V[] val; + protected int size; + + public Int2ObjectArraySortedMap() { + this.key = new int[8]; + this.val = (V[])new Object[8]; + } + + public V put(final int key, final V value) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index >= 0) { + final V current = this.val[index]; + this.val[index] = value; + return current; + } + final int insert = -(index + 1); + // shift entries down + if (this.size >= this.val.length) { + this.key = Arrays.copyOf(this.key, this.key.length * 2); + this.val = Arrays.copyOf(this.val, this.val.length * 2); + } + System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); + System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); + + this.key[insert] = key; + this.val[insert] = value; + + return null; + } + + public V computeIfAbsent(final int key, final IntFunction producer) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index >= 0) { + return this.val[index]; + } + final int insert = -(index + 1); + // shift entries down + if (this.size >= this.val.length) { + this.key = Arrays.copyOf(this.key, this.key.length * 2); + this.val = Arrays.copyOf(this.val, this.val.length * 2); + } + System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); + System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); + + this.key[insert] = key; + + return this.val[insert] = producer.apply(key); + } + + public V get(final int key) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index < 0) { + return null; + } + return this.val[index]; + } + + public V getFloor(final int key) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index < 0) { + final int insert = -(index + 1); + return this.val[insert]; + } + return this.val[index]; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java new file mode 100644 index 0000000..c077ca6 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java @@ -0,0 +1,77 @@ +package ca.spottedleaf.moonrise.common.map; + +import it.unimi.dsi.fastutil.longs.Long2IntFunction; + +import java.util.Arrays; + +public class Long2IntArraySortedMap { + + protected long[] key; + protected int[] val; + protected int size; + + public Long2IntArraySortedMap() { + this.key = new long[8]; + this.val = new int[8]; + } + + public int put(final long key, final int value) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index >= 0) { + final int current = this.val[index]; + this.val[index] = value; + return current; + } + final int insert = -(index + 1); + // shift entries down + if (this.size >= this.val.length) { + this.key = Arrays.copyOf(this.key, this.key.length * 2); + this.val = Arrays.copyOf(this.val, this.val.length * 2); + } + System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); + System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); + ++this.size; + + this.key[insert] = key; + this.val[insert] = value; + + return 0; + } + + public int computeIfAbsent(final long key, final Long2IntFunction producer) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index >= 0) { + return this.val[index]; + } + final int insert = -(index + 1); + // shift entries down + if (this.size >= this.val.length) { + this.key = Arrays.copyOf(this.key, this.key.length * 2); + this.val = Arrays.copyOf(this.val, this.val.length * 2); + } + System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); + System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); + ++this.size; + + this.key[insert] = key; + + return this.val[insert] = producer.apply(key); + } + + public int get(final long key) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index < 0) { + return 0; + } + return this.val[index]; + } + + public int getFloor(final long key) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index < 0) { + final int insert = -(index + 1) - 1; + return insert < 0 ? 0 : this.val[insert]; + } + return this.val[index]; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java new file mode 100644 index 0000000..b24d037 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java @@ -0,0 +1,76 @@ +package ca.spottedleaf.moonrise.common.map; + +import java.util.Arrays; +import java.util.function.LongFunction; + +public class Long2ObjectArraySortedMap { + + protected long[] key; + protected V[] val; + protected int size; + + public Long2ObjectArraySortedMap() { + this.key = new long[8]; + this.val = (V[])new Object[8]; + } + + public V put(final long key, final V value) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index >= 0) { + final V current = this.val[index]; + this.val[index] = value; + return current; + } + final int insert = -(index + 1); + // shift entries down + if (this.size >= this.val.length) { + this.key = Arrays.copyOf(this.key, this.key.length * 2); + this.val = Arrays.copyOf(this.val, this.val.length * 2); + } + System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); + System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); + ++this.size; + + this.key[insert] = key; + this.val[insert] = value; + + return null; + } + + public V computeIfAbsent(final long key, final LongFunction producer) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index >= 0) { + return this.val[index]; + } + final int insert = -(index + 1); + // shift entries down + if (this.size >= this.val.length) { + this.key = Arrays.copyOf(this.key, this.key.length * 2); + this.val = Arrays.copyOf(this.val, this.val.length * 2); + } + System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); + System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); + ++this.size; + + this.key[insert] = key; + + return this.val[insert] = producer.apply(key); + } + + public V get(final long key) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index < 0) { + return null; + } + return this.val[index]; + } + + public V getFloor(final long key) { + final int index = Arrays.binarySearch(this.key, 0, this.size, key); + if (index < 0) { + final int insert = -(index + 1) - 1; + return insert < 0 ? null : this.val[insert]; + } + return this.val[index]; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java new file mode 100644 index 0000000..460e27a --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java @@ -0,0 +1,297 @@ +package ca.spottedleaf.moonrise.common.misc; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; + +public final class Delayed26WayDistancePropagator3D { + + // this map is considered "stale" unless updates are propagated. + protected final Delayed8WayDistancePropagator2D.LevelMap levels = new Delayed8WayDistancePropagator2D.LevelMap(8192*2, 0.6f); + + // this map is never stale + protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f); + + // Generally updates to positions are made close to other updates, so we link to decrease cache misses when + // propagating updates + protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet(); + + @FunctionalInterface + public static interface LevelChangeCallback { + + /** + * This can be called for intermediate updates. So do not rely on newLevel being close to or + * the exact level that is expected after a full propagation has occured. + */ + public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel); + + } + + protected final LevelChangeCallback changeCallback; + + public Delayed26WayDistancePropagator3D() { + this(null); + } + + public Delayed26WayDistancePropagator3D(final LevelChangeCallback changeCallback) { + this.changeCallback = changeCallback; + } + + public int getLevel(final long pos) { + return this.levels.get(pos); + } + + public int getLevel(final int x, final int y, final int z) { + return this.levels.get(CoordinateUtils.getChunkSectionKey(x, y, z)); + } + + public void setSource(final int x, final int y, final int z, final int level) { + this.setSource(CoordinateUtils.getChunkSectionKey(x, y, z), level); + } + + public void setSource(final long coordinate, final int level) { + if ((level & 63) != level || level == 0) { + throw new IllegalArgumentException("Level must be in (0, 63], not " + level); + } + + final byte byteLevel = (byte)level; + final byte oldLevel = this.sources.put(coordinate, byteLevel); + + if (oldLevel == byteLevel) { + return; // nothing to do + } + + // queue to update later + this.updatedSources.add(coordinate); + } + + public void removeSource(final int x, final int y, final int z) { + this.removeSource(CoordinateUtils.getChunkSectionKey(x, y, z)); + } + + public void removeSource(final long coordinate) { + if (this.sources.remove(coordinate) != 0) { + this.updatedSources.add(coordinate); + } + } + + // queues used for BFS propagating levels + protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelIncreaseWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64]; + { + for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) { + this.levelIncreaseWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue(); + } + } + protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelRemoveWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64]; + { + for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) { + this.levelRemoveWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue(); + } + } + protected long levelIncreaseWorkQueueBitset; + protected long levelRemoveWorkQueueBitset; + + protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) { + final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[level]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelIncreaseWorkQueueBitset |= (1L << level); + } + + protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) { + final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[index]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelIncreaseWorkQueueBitset |= (1L << index); + } + + protected final void addToRemoveWorkQueue(final long coordinate, final byte level) { + final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[level]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelRemoveWorkQueueBitset |= (1L << level); + } + + public boolean propagateUpdates() { + if (this.updatedSources.isEmpty()) { + return false; + } + + boolean ret = false; + + for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) { + final long coordinate = iterator.nextLong(); + + final byte currentLevel = this.levels.get(coordinate); + final byte updatedSource = this.sources.get(coordinate); + + if (currentLevel == updatedSource) { + continue; + } + ret = true; + + if (updatedSource > currentLevel) { + // level increase + this.addToIncreaseWorkQueue(coordinate, updatedSource); + } else { + // level decrease + this.addToRemoveWorkQueue(coordinate, currentLevel); + // if the current coordinate is a source, then the decrease propagation will detect that and queue + // the source propagation + } + } + + this.updatedSources.clear(); + + // propagate source level increases first for performance reasons (in crowded areas hopefully the additions + // make the removes remove less) + this.propagateIncreases(); + + // now we propagate the decreases (which will then re-propagate clobbered sources) + this.propagateDecreases(); + + return ret; + } + + protected void propagateIncreases() { + for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset); + this.levelIncreaseWorkQueueBitset != 0L; + this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) { + + final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex]; + while (!queue.queuedLevels.isEmpty()) { + final long coordinate = queue.queuedCoordinates.removeFirstLong(); + byte level = queue.queuedLevels.removeFirstByte(); + + final boolean neighbourCheck = level < 0; + + final byte currentLevel; + if (neighbourCheck) { + level = (byte)-level; + currentLevel = this.levels.get(coordinate); + } else { + currentLevel = this.levels.putIfGreater(coordinate, level); + } + + if (neighbourCheck) { + // used when propagating from decrease to indicate that this level needs to check its neighbours + // this means the level at coordinate could be equal, but would still need neighbours checked + + if (currentLevel != level) { + // something caused the level to change, which means something propagated to it (which means + // us propagating here is redundant), or something removed the level (which means we + // cannot propagate further) + continue; + } + } else if (currentLevel >= level) { + // something higher/equal propagated + continue; + } + if (this.changeCallback != null) { + this.changeCallback.onLevelUpdate(coordinate, currentLevel, level); + } + + if (level == 1) { + // can't propagate 0 to neighbours + continue; + } + + // propagate to neighbours + final byte neighbourLevel = (byte)(level - 1); + final int x = CoordinateUtils.getChunkSectionX(coordinate); + final int y = CoordinateUtils.getChunkSectionY(coordinate); + final int z = CoordinateUtils.getChunkSectionZ(coordinate); + + for (int dy = -1; dy <= 1; ++dy) { + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + if ((dy | dz | dx) == 0) { + // already propagated to coordinate + continue; + } + + // sure we can check the neighbour level in the map right now and avoid a propagation, + // but then we would still have to recheck it when popping the value off of the queue! + // so just avoid the double lookup + final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z); + this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel); + } + } + } + } + } + } + + protected void propagateDecreases() { + for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset); + this.levelRemoveWorkQueueBitset != 0L; + this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) { + + final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[queueIndex]; + while (!queue.queuedLevels.isEmpty()) { + final long coordinate = queue.queuedCoordinates.removeFirstLong(); + final byte level = queue.queuedLevels.removeFirstByte(); + + final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level); + if (currentLevel == 0) { + // something else removed + continue; + } + + if (currentLevel > level) { + // something higher propagated here or we hit the propagation of another source + // in the second case we need to re-propagate because we could have just clobbered another source's + // propagation + this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking + continue; + } + + if (this.changeCallback != null) { + this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0); + } + + final byte source = this.sources.get(coordinate); + if (source != 0) { + // must re-propagate source later + this.addToIncreaseWorkQueue(coordinate, source); + } + + if (level == 0) { + // can't propagate -1 to neighbours + // we have to check neighbours for removing 1 just in case the neighbour is 2 + continue; + } + + // propagate to neighbours + final byte neighbourLevel = (byte)(level - 1); + final int x = CoordinateUtils.getChunkSectionX(coordinate); + final int y = CoordinateUtils.getChunkSectionY(coordinate); + final int z = CoordinateUtils.getChunkSectionZ(coordinate); + + for (int dy = -1; dy <= 1; ++dy) { + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + if ((dy | dz | dx) == 0) { + // already propagated to coordinate + continue; + } + + // sure we can check the neighbour level in the map right now and avoid a propagation, + // but then we would still have to recheck it when popping the value off of the queue! + // so just avoid the double lookup + final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z); + this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel); + } + } + } + } + } + + // propagate sources we clobbered in the process + this.propagateIncreases(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java new file mode 100644 index 0000000..ab2fa15 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java @@ -0,0 +1,718 @@ +package ca.spottedleaf.moonrise.common.misc; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import it.unimi.dsi.fastutil.HashCommon; +import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; + +public final class Delayed8WayDistancePropagator2D { + + // Test + /* + protected static void test(int x, int z, com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap reference, Delayed8WayDistancePropagator2D test) { + int got = test.getLevel(x, z); + + int expect = 0; + Object[] nearest = reference.getObjectsInRange(x, z) == null ? null : reference.getObjectsInRange(x, z).getBackingSet(); + if (nearest != null) { + for (Object _obj : nearest) { + if (_obj instanceof Ticket) { + Ticket ticket = (Ticket)_obj; + long ticketCoord = reference.getLastCoordinate(ticket); + int viewDistance = reference.getLastViewDistance(ticket); + int distance = Math.max(com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateX(ticketCoord) - x), + com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateZ(ticketCoord) - z)); + int level = viewDistance - distance; + if (level > expect) { + expect = level; + } + } + } + } + + if (expect != got) { + throw new IllegalStateException("Expected " + expect + " at pos (" + x + "," + z + ") but got " + got); + } + } + + static class Ticket { + + int x; + int z; + + final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet empty + = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this); + + } + + public static void main(final String[] args) { + com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap reference = new com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap() { + @Override + protected com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getEmptySetFor(Ticket object) { + return object.empty; + } + }; + Delayed8WayDistancePropagator2D test = new Delayed8WayDistancePropagator2D(); + + final int maxDistance = 64; + // test origin + { + Ticket originTicket = new Ticket(); + int originDistance = 31; + // test single source + reference.add(originTicket, 0, 0, originDistance); + test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate + for (int dx = -originDistance; dx <= originDistance; ++dx) { + for (int dz = -originDistance; dz <= originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + // test single source decrease + reference.update(originTicket, 0, 0, originDistance/2); + test.setSource(0, 0, originDistance/2); test.propagateUpdates(); // set and propagate + for (int dx = -originDistance; dx <= originDistance; ++dx) { + for (int dz = -originDistance; dz <= originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + // test source increase + originDistance = 2*originDistance; + reference.update(originTicket, 0, 0, originDistance); + test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate + for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) { + for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + reference.remove(originTicket); + test.removeSource(0, 0); test.propagateUpdates(); + } + + // test multiple sources at origin + { + int originDistance = 31; + java.util.List list = new java.util.ArrayList<>(); + for (int i = 0; i < 10; ++i) { + Ticket a = new Ticket(); + list.add(a); + a.x = (i & 1) == 1 ? -i : i; + a.z = (i & 1) == 1 ? -i : i; + } + for (Ticket ticket : list) { + reference.add(ticket, ticket.x, ticket.z, originDistance); + test.setSource(ticket.x, ticket.z, originDistance); + } + test.propagateUpdates(); + + for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { + for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket level decrease + + for (Ticket ticket : list) { + reference.update(ticket, ticket.x, ticket.z, originDistance/2); + test.setSource(ticket.x, ticket.z, originDistance/2); + } + test.propagateUpdates(); + + for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { + for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket level increase + + for (Ticket ticket : list) { + reference.update(ticket, ticket.x, ticket.z, originDistance*2); + test.setSource(ticket.x, ticket.z, originDistance*2); + } + test.propagateUpdates(); + + for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { + for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket remove + for (int i = 0, len = list.size(); i < len; ++i) { + if ((i & 3) != 0) { + continue; + } + Ticket ticket = list.get(i); + reference.remove(ticket); + test.removeSource(ticket.x, ticket.z); + } + test.propagateUpdates(); + + for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { + for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + } + + // now test at coordinate offsets + // test offset + { + Ticket originTicket = new Ticket(); + int originDistance = 31; + int offX = 54432; + int offZ = -134567; + // test single source + reference.add(originTicket, offX, offZ, originDistance); + test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate + for (int dx = -originDistance; dx <= originDistance; ++dx) { + for (int dz = -originDistance; dz <= originDistance; ++dz) { + test(dx + offX, dz + offZ, reference, test); + } + } + // test single source decrease + reference.update(originTicket, offX, offZ, originDistance/2); + test.setSource(offX, offZ, originDistance/2); test.propagateUpdates(); // set and propagate + for (int dx = -originDistance; dx <= originDistance; ++dx) { + for (int dz = -originDistance; dz <= originDistance; ++dz) { + test(dx + offX, dz + offZ, reference, test); + } + } + // test source increase + originDistance = 2*originDistance; + reference.update(originTicket, offX, offZ, originDistance); + test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate + for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) { + for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) { + test(dx + offX, dz + offZ, reference, test); + } + } + + reference.remove(originTicket); + test.removeSource(offX, offZ); test.propagateUpdates(); + } + + // test multiple sources at origin + { + int originDistance = 31; + int offX = 54432; + int offZ = -134567; + java.util.List list = new java.util.ArrayList<>(); + for (int i = 0; i < 10; ++i) { + Ticket a = new Ticket(); + list.add(a); + a.x = offX + ((i & 1) == 1 ? -i : i); + a.z = offZ + ((i & 1) == 1 ? -i : i); + } + for (Ticket ticket : list) { + reference.add(ticket, ticket.x, ticket.z, originDistance); + test.setSource(ticket.x, ticket.z, originDistance); + } + test.propagateUpdates(); + + for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { + for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket level decrease + + for (Ticket ticket : list) { + reference.update(ticket, ticket.x, ticket.z, originDistance/2); + test.setSource(ticket.x, ticket.z, originDistance/2); + } + test.propagateUpdates(); + + for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { + for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket level increase + + for (Ticket ticket : list) { + reference.update(ticket, ticket.x, ticket.z, originDistance*2); + test.setSource(ticket.x, ticket.z, originDistance*2); + } + test.propagateUpdates(); + + for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { + for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket remove + for (int i = 0, len = list.size(); i < len; ++i) { + if ((i & 3) != 0) { + continue; + } + Ticket ticket = list.get(i); + reference.remove(ticket); + test.removeSource(ticket.x, ticket.z); + } + test.propagateUpdates(); + + for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { + for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + } + } + */ + + // this map is considered "stale" unless updates are propagated. + protected final LevelMap levels = new LevelMap(8192*2, 0.6f); + + // this map is never stale + protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f); + + // Generally updates to positions are made close to other updates, so we link to decrease cache misses when + // propagating updates + protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet(); + + @FunctionalInterface + public static interface LevelChangeCallback { + + /** + * This can be called for intermediate updates. So do not rely on newLevel being close to or + * the exact level that is expected after a full propagation has occured. + */ + public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel); + + } + + protected final LevelChangeCallback changeCallback; + + public Delayed8WayDistancePropagator2D() { + this(null); + } + + public Delayed8WayDistancePropagator2D(final LevelChangeCallback changeCallback) { + this.changeCallback = changeCallback; + } + + public int getLevel(final long pos) { + return this.levels.get(pos); + } + + public int getLevel(final int x, final int z) { + return this.levels.get(CoordinateUtils.getChunkKey(x, z)); + } + + public void setSource(final int x, final int z, final int level) { + this.setSource(CoordinateUtils.getChunkKey(x, z), level); + } + + public void setSource(final long coordinate, final int level) { + if ((level & 63) != level || level == 0) { + throw new IllegalArgumentException("Level must be in (0, 63], not " + level); + } + + final byte byteLevel = (byte)level; + final byte oldLevel = this.sources.put(coordinate, byteLevel); + + if (oldLevel == byteLevel) { + return; // nothing to do + } + + // queue to update later + this.updatedSources.add(coordinate); + } + + public void removeSource(final int x, final int z) { + this.removeSource(CoordinateUtils.getChunkKey(x, z)); + } + + public void removeSource(final long coordinate) { + if (this.sources.remove(coordinate) != 0) { + this.updatedSources.add(coordinate); + } + } + + // queues used for BFS propagating levels + protected final WorkQueue[] levelIncreaseWorkQueues = new WorkQueue[64]; + { + for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) { + this.levelIncreaseWorkQueues[i] = new WorkQueue(); + } + } + protected final WorkQueue[] levelRemoveWorkQueues = new WorkQueue[64]; + { + for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) { + this.levelRemoveWorkQueues[i] = new WorkQueue(); + } + } + protected long levelIncreaseWorkQueueBitset; + protected long levelRemoveWorkQueueBitset; + + protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) { + final WorkQueue queue = this.levelIncreaseWorkQueues[level]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelIncreaseWorkQueueBitset |= (1L << level); + } + + protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) { + final WorkQueue queue = this.levelIncreaseWorkQueues[index]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelIncreaseWorkQueueBitset |= (1L << index); + } + + protected final void addToRemoveWorkQueue(final long coordinate, final byte level) { + final WorkQueue queue = this.levelRemoveWorkQueues[level]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelRemoveWorkQueueBitset |= (1L << level); + } + + public boolean propagateUpdates() { + if (this.updatedSources.isEmpty()) { + return false; + } + + boolean ret = false; + + for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) { + final long coordinate = iterator.nextLong(); + + final byte currentLevel = this.levels.get(coordinate); + final byte updatedSource = this.sources.get(coordinate); + + if (currentLevel == updatedSource) { + continue; + } + ret = true; + + if (updatedSource > currentLevel) { + // level increase + this.addToIncreaseWorkQueue(coordinate, updatedSource); + } else { + // level decrease + this.addToRemoveWorkQueue(coordinate, currentLevel); + // if the current coordinate is a source, then the decrease propagation will detect that and queue + // the source propagation + } + } + + this.updatedSources.clear(); + + // propagate source level increases first for performance reasons (in crowded areas hopefully the additions + // make the removes remove less) + this.propagateIncreases(); + + // now we propagate the decreases (which will then re-propagate clobbered sources) + this.propagateDecreases(); + + return ret; + } + + protected void propagateIncreases() { + for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset); + this.levelIncreaseWorkQueueBitset != 0L; + this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) { + + final WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex]; + while (!queue.queuedLevels.isEmpty()) { + final long coordinate = queue.queuedCoordinates.removeFirstLong(); + byte level = queue.queuedLevels.removeFirstByte(); + + final boolean neighbourCheck = level < 0; + + final byte currentLevel; + if (neighbourCheck) { + level = (byte)-level; + currentLevel = this.levels.get(coordinate); + } else { + currentLevel = this.levels.putIfGreater(coordinate, level); + } + + if (neighbourCheck) { + // used when propagating from decrease to indicate that this level needs to check its neighbours + // this means the level at coordinate could be equal, but would still need neighbours checked + + if (currentLevel != level) { + // something caused the level to change, which means something propagated to it (which means + // us propagating here is redundant), or something removed the level (which means we + // cannot propagate further) + continue; + } + } else if (currentLevel >= level) { + // something higher/equal propagated + continue; + } + if (this.changeCallback != null) { + this.changeCallback.onLevelUpdate(coordinate, currentLevel, level); + } + + if (level == 1) { + // can't propagate 0 to neighbours + continue; + } + + // propagate to neighbours + final byte neighbourLevel = (byte)(level - 1); + final int x = (int)coordinate; + final int z = (int)(coordinate >>> 32); + + for (int dx = -1; dx <= 1; ++dx) { + for (int dz = -1; dz <= 1; ++dz) { + if ((dx | dz) == 0) { + // already propagated to coordinate + continue; + } + + // sure we can check the neighbour level in the map right now and avoid a propagation, + // but then we would still have to recheck it when popping the value off of the queue! + // so just avoid the double lookup + final long neighbourCoordinate = CoordinateUtils.getChunkKey(x + dx, z + dz); + this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel); + } + } + } + } + } + + protected void propagateDecreases() { + for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset); + this.levelRemoveWorkQueueBitset != 0L; + this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) { + + final WorkQueue queue = this.levelRemoveWorkQueues[queueIndex]; + while (!queue.queuedLevels.isEmpty()) { + final long coordinate = queue.queuedCoordinates.removeFirstLong(); + final byte level = queue.queuedLevels.removeFirstByte(); + + final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level); + if (currentLevel == 0) { + // something else removed + continue; + } + + if (currentLevel > level) { + // something higher propagated here or we hit the propagation of another source + // in the second case we need to re-propagate because we could have just clobbered another source's + // propagation + this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking + continue; + } + + if (this.changeCallback != null) { + this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0); + } + + final byte source = this.sources.get(coordinate); + if (source != 0) { + // must re-propagate source later + this.addToIncreaseWorkQueue(coordinate, source); + } + + if (level == 0) { + // can't propagate -1 to neighbours + // we have to check neighbours for removing 1 just in case the neighbour is 2 + continue; + } + + // propagate to neighbours + final byte neighbourLevel = (byte)(level - 1); + final int x = (int)coordinate; + final int z = (int)(coordinate >>> 32); + + for (int dx = -1; dx <= 1; ++dx) { + for (int dz = -1; dz <= 1; ++dz) { + if ((dx | dz) == 0) { + // already propagated to coordinate + continue; + } + + // sure we can check the neighbour level in the map right now and avoid a propagation, + // but then we would still have to recheck it when popping the value off of the queue! + // so just avoid the double lookup + final long neighbourCoordinate = CoordinateUtils.getChunkKey(x + dx, z + dz); + this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel); + } + } + } + } + + // propagate sources we clobbered in the process + this.propagateIncreases(); + } + + protected static final class LevelMap extends Long2ByteOpenHashMap { + public LevelMap() { + super(); + } + + public LevelMap(final int expected, final float loadFactor) { + super(expected, loadFactor); + } + + // copied from superclass + private int find(final long k) { + if (k == 0L) { + return this.containsNullKey ? this.n : -(this.n + 1); + } else { + final long[] key = this.key; + long curr; + int pos; + if ((curr = key[pos = (int)HashCommon.mix(k) & this.mask]) == 0L) { + return -(pos + 1); + } else if (k == curr) { + return pos; + } else { + while((curr = key[pos = pos + 1 & this.mask]) != 0L) { + if (k == curr) { + return pos; + } + } + + return -(pos + 1); + } + } + } + + // copied from superclass + private void insert(final int pos, final long k, final byte v) { + if (pos == this.n) { + this.containsNullKey = true; + } + + this.key[pos] = k; + this.value[pos] = v; + if (this.size++ >= this.maxFill) { + this.rehash(HashCommon.arraySize(this.size + 1, this.f)); + } + } + + // copied from superclass + public byte putIfGreater(final long key, final byte value) { + final int pos = this.find(key); + if (pos < 0) { + if (this.defRetValue < value) { + this.insert(-pos - 1, key, value); + } + return this.defRetValue; + } else { + final byte curr = this.value[pos]; + if (value > curr) { + this.value[pos] = value; + return curr; + } + return curr; + } + } + + // copied from superclass + private void removeEntry(final int pos) { + --this.size; + this.shiftKeys(pos); + if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) { + this.rehash(this.n / 2); + } + } + + // copied from superclass + private void removeNullEntry() { + this.containsNullKey = false; + --this.size; + if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) { + this.rehash(this.n / 2); + } + } + + // copied from superclass + public byte removeIfGreaterOrEqual(final long key, final byte value) { + if (key == 0L) { + if (!this.containsNullKey) { + return this.defRetValue; + } + final byte current = this.value[this.n]; + if (value >= current) { + this.removeNullEntry(); + return current; + } + return current; + } else { + long[] keys = this.key; + byte[] values = this.value; + long curr; + int pos; + if ((curr = keys[pos = (int)HashCommon.mix(key) & this.mask]) == 0L) { + return this.defRetValue; + } else if (key == curr) { + final byte current = values[pos]; + if (value >= current) { + this.removeEntry(pos); + return current; + } + return current; + } else { + while((curr = keys[pos = pos + 1 & this.mask]) != 0L) { + if (key == curr) { + final byte current = values[pos]; + if (value >= current) { + this.removeEntry(pos); + return current; + } + return current; + } + } + + return this.defRetValue; + } + } + } + } + + protected static final class WorkQueue { + + public final NoResizeLongArrayFIFODeque queuedCoordinates = new NoResizeLongArrayFIFODeque(); + public final NoResizeByteArrayFIFODeque queuedLevels = new NoResizeByteArrayFIFODeque(); + + } + + protected static final class NoResizeLongArrayFIFODeque extends LongArrayFIFOQueue { + + /** + * Assumes non-empty. If empty, undefined behaviour. + */ + public long removeFirstLong() { + // copied from superclass + long t = this.array[this.start]; + if (++this.start == this.length) { + this.start = 0; + } + + return t; + } + } + + protected static final class NoResizeByteArrayFIFODeque extends ByteArrayFIFOQueue { + + /** + * Assumes non-empty. If empty, undefined behaviour. + */ + public byte removeFirstByte() { + // copied from superclass + byte t = this.array[this.start]; + if (++this.start == this.length) { + this.start = 0; + } + + return t; + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java new file mode 100644 index 0000000..4123edd --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java @@ -0,0 +1,68 @@ +package ca.spottedleaf.moonrise.common.set; + +import java.util.Collection; + +public final class OptimizedSmallEnumSet> { + + private final Class enumClass; + private long backingSet; + + public OptimizedSmallEnumSet(final Class clazz) { + if (clazz == null) { + throw new IllegalArgumentException("Null class"); + } + if (!clazz.isEnum()) { + throw new IllegalArgumentException("Class must be enum, not " + clazz.getCanonicalName()); + } + this.enumClass = clazz; + } + + public boolean addUnchecked(final E element) { + final int ordinal = element.ordinal(); + final long key = 1L << ordinal; + + final long prev = this.backingSet; + this.backingSet = prev | key; + + return (prev & key) == 0; + } + + public boolean removeUnchecked(final E element) { + final int ordinal = element.ordinal(); + final long key = 1L << ordinal; + + final long prev = this.backingSet; + this.backingSet = prev & ~key; + + return (prev & key) != 0; + } + + public void clear() { + this.backingSet = 0L; + } + + public int size() { + return Long.bitCount(this.backingSet); + } + + public void addAllUnchecked(final Collection enums) { + for (final E element : enums) { + if (element == null) { + throw new NullPointerException("Null element"); + } + this.backingSet |= (1L << element.ordinal()); + } + } + + public long getBackingSet() { + return this.backingSet; + } + + public boolean hasCommonElements(final OptimizedSmallEnumSet other) { + return (other.backingSet & this.backingSet) != 0; + } + + public boolean hasElement(final E element) { + return (this.backingSet & (1L << element.ordinal())) != 0; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java new file mode 100644 index 0000000..83602ae --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java @@ -0,0 +1,153 @@ +package ca.spottedleaf.moonrise.common.util; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.phys.Vec3; + +public final class CoordinateUtils { + + // dx, dz are relative to the target chunk + // dx, dz in [-radius, radius] + public static int getNeighbourMappedIndex(final int dx, final int dz, final int radius) { + return (dx + radius) + (2 * radius + 1)*(dz + radius); + } + + // the chunk keys are compatible with vanilla + + public static long getChunkKey(final BlockPos pos) { + return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL); + } + + public static long getChunkKey(final Entity entity) { + return ((Mth.lfloor(entity.getZ()) >> 4) << 32) | ((Mth.lfloor(entity.getX()) >> 4) & 0xFFFFFFFFL); + } + + public static long getChunkKey(final ChunkPos pos) { + return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL); + } + + public static long getChunkKey(final SectionPos pos) { + return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL); + } + + public static long getChunkKey(final int x, final int z) { + return ((long)z << 32) | (x & 0xFFFFFFFFL); + } + + public static int getChunkX(final long chunkKey) { + return (int)chunkKey; + } + + public static int getChunkZ(final long chunkKey) { + return (int)(chunkKey >>> 32); + } + + public static int getChunkCoordinate(final double blockCoordinate) { + return Mth.floor(blockCoordinate) >> 4; + } + + // the section keys are compatible with vanilla's + + static final int SECTION_X_BITS = 22; + static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1; + static final int SECTION_Y_BITS = 20; + static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1; + static final int SECTION_Z_BITS = 22; + static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1; + // format is y,z,x (in order of LSB to MSB) + static final int SECTION_Y_SHIFT = 0; + static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS; + static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS; + static final int SECTION_TO_BLOCK_SHIFT = 4; + + public static long getChunkSectionKey(final int x, final int y, final int z) { + return ((x & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static long getChunkSectionKey(final SectionPos pos) { + return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static long getChunkSectionKey(final ChunkPos pos, final int y) { + return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static long getChunkSectionKey(final BlockPos pos) { + return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | + ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | + (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT)); + } + + public static long getChunkSectionKey(final Entity entity) { + return ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | + ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | + ((Mth.lfloor(entity.getZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT)); + } + + public static int getChunkSectionX(final long key) { + return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS)); + } + + public static int getChunkSectionY(final long key) { + return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS)); + } + + public static int getChunkSectionZ(final long key) { + return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS)); + } + + // the block coordinates are not necessarily compatible with vanilla's + + public static int getBlockCoordinate(final double blockCoordinate) { + return Mth.floor(blockCoordinate); + } + + public static long getBlockKey(final int x, final int y, final int z) { + return ((long)x & 0x7FFFFFF) | (((long)z & 0x7FFFFFF) << 27) | ((long)y << 54); + } + + public static long getBlockKey(final BlockPos pos) { + return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54); + } + + public static long getBlockKey(final Entity entity) { + return ((long)entity.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54); + } + + public static int getBlockX(final Vec3 pos) { + return Mth.floor(pos.x); + } + + public static int getBlockY(final Vec3 pos) { + return Mth.floor(pos.y); + } + + public static int getBlockZ(final Vec3 pos) { + return Mth.floor(pos.z); + } + + public static int getChunkX(final Vec3 pos) { + return Mth.floor(pos.x) >> 4; + } + + public static int getChunkY(final Vec3 pos) { + return Mth.floor(pos.y) >> 4; + } + + public static int getChunkZ(final Vec3 pos) { + return Mth.floor(pos.z) >> 4; + } + + private CoordinateUtils() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java new file mode 100644 index 0000000..21a3525 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java @@ -0,0 +1,46 @@ +package ca.spottedleaf.moonrise.common.util; + +import net.minecraft.world.level.LevelHeightAccessor; + +public final class WorldUtil { + + // min, max are inclusive + + public static int getMaxSection(final LevelHeightAccessor world) { + return world.getMaxSection() - 1; // getMaxSection() is exclusive + } + + public static int getMinSection(final LevelHeightAccessor world) { + return world.getMinSection(); + } + + public static int getMaxLightSection(final LevelHeightAccessor world) { + return getMaxSection(world) + 1; + } + + public static int getMinLightSection(final LevelHeightAccessor world) { + return getMinSection(world) - 1; + } + + + + public static int getTotalSections(final LevelHeightAccessor world) { + return getMaxSection(world) - getMinSection(world) + 1; + } + + public static int getTotalLightSections(final LevelHeightAccessor world) { + return getMaxLightSection(world) - getMinLightSection(world) + 1; + } + + public static int getMinBlockY(final LevelHeightAccessor world) { + return getMinSection(world) << 4; + } + + public static int getMaxBlockY(final LevelHeightAccessor world) { + return (getMaxSection(world) << 4) | 15; + } + + private WorldUtil() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/bitstorage/SimpleBitStorageMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/bitstorage/SimpleBitStorageMixin.java new file mode 100644 index 0000000..90cf697 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/bitstorage/SimpleBitStorageMixin.java @@ -0,0 +1,138 @@ +package ca.spottedleaf.moonrise.mixin.bitstorage; + +import ca.spottedleaf.concurrentutil.util.IntegerUtil; +import net.minecraft.util.BitStorage; +import net.minecraft.util.SimpleBitStorage; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(SimpleBitStorage.class) +public abstract class SimpleBitStorageMixin implements BitStorage { + + @Shadow + @Final + private int bits; + + @Shadow + @Final + private long[] data; + + @Shadow + @Final + private int valuesPerLong; + + + + /* + This is how the indices are supposed to be computed: + final int dataIndex = index / this.valuesPerLong; + final int localIndex = (index % this.valuesPerLong) * this.bitsPerValue; + where valuesPerLong = 64 / this.bits + The additional add that mojang uses is only for unsigned division, when in reality the above is signed division. + Thus, it is appropriate to use the signed division magic values which do not use an add. + */ + + + + @Unique + private static final long[] BETTER_MAGIC = new long[33]; + static { + for (int i = 1; i < BETTER_MAGIC.length; ++i) { + BETTER_MAGIC[i] = IntegerUtil.getDivisorNumbers(64 / i); + } + } + + @Unique + private long magic; + + /** + * @reason Init magic field + * @author Spottedleaf + */ + @Inject( + method = "(II[J)V", + at = @At( + value = "RETURN" + ) + ) + private void init(final CallbackInfo ci) { + this.magic = BETTER_MAGIC[this.bits]; + } + + /** + * @reason Optimise method to use our magic value, which does not perform an add + * @author Spottedleaf + */ + @Overwrite + @Override + public int getAndSet(final int index, final int value) { + // assume index/value in range + final long magic = this.magic; + final int bits = this.bits; + final long mul = magic >>> 32; + final int dataIndex = (int)(((long)index * mul) >>> magic); + + final long[] dataArray = this.data; + + final long data = dataArray[dataIndex]; + final long mask = (1L << bits) - 1; // avoid extra memory read + + + final int bitIndex = (index - (dataIndex * this.valuesPerLong)) * bits; + final int prev = (int)(data >> bitIndex & mask); + + dataArray[dataIndex] = data & ~(mask << bitIndex) | ((long)value & mask) << bitIndex; + + return prev; + } + + /** + * @reason Optimise method to use our magic value, which does not perform an add + * @author Spottedleaf + */ + @Overwrite + @Override + public void set(final int index, final int value) { + // assume index/value in range + final long magic = this.magic; + final int bits = this.bits; + final long mul = magic >>> 32; + final int dataIndex = (int)(((long)index * mul) >>> magic); + + final long[] dataArray = this.data; + + final long data = dataArray[dataIndex]; + final long mask = (1L << bits) - 1; // avoid extra memory read + + final int bitIndex = (index - (dataIndex * this.valuesPerLong)) * bits; + + dataArray[dataIndex] = data & ~(mask << bitIndex) | ((long)value & mask) << bitIndex; + } + + /** + * @reason Optimise method to use our magic value, which does not perform an add + * @author Spottedleaf + */ + @Overwrite + @Override + public int get(final int index) { + // assume index in range + final long magic = this.magic; + final int bits = this.bits; + final long mul = magic >>> 32; + final int dataIndex = (int)(((long)index * mul) >>> magic); + + final long mask = (1L << bits) - 1; // avoid extra memory read + final long data = this.data[dataIndex]; + + final int bitIndex = (index - (dataIndex * this.valuesPerLong)) * bits; + + return (int)(data >> bitIndex & mask); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/bitstorage/ZeroBitStorageMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/bitstorage/ZeroBitStorageMixin.java new file mode 100644 index 0000000..3c95df3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/bitstorage/ZeroBitStorageMixin.java @@ -0,0 +1,40 @@ +package ca.spottedleaf.moonrise.mixin.bitstorage; + +import net.minecraft.util.BitStorage; +import net.minecraft.util.ZeroBitStorage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +@Mixin(ZeroBitStorage.class) +public abstract class ZeroBitStorageMixin implements BitStorage { + + /** + * @reason Do not validate input + * @author Spottedleaf + */ + @Overwrite + @Override + public int getAndSet(final int index, final int value) { + return 0; + } + + /** + * @reason Do not validate input + * @author Spottedleaf + */ + @Overwrite + @Override + public void set(final int index, final int value) { + + } + + /** + * @reason Do not validate input + * @author Spottedleaf + */ + @Overwrite + @Override + public int get(final int index) { + return 0; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_getblock/LevelChunkMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_getblock/LevelChunkMixin.java new file mode 100644 index 0000000..65fe6a8 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_getblock/LevelChunkMixin.java @@ -0,0 +1,149 @@ +package ca.spottedleaf.moonrise.mixin.chunk_getblock; + +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_getblock.GetBlockChunk; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Registry; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.EmptyLevelChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.levelgen.DebugLevelSource; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.ticks.LevelChunkTicks; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LevelChunk.class) +public abstract class LevelChunkMixin extends ChunkAccess implements GetBlockChunk { + + @Shadow + @Final + Level level; + + + @Unique + private static final BlockState AIR_BLOCKSTATE = Blocks.AIR.defaultBlockState(); + @Unique + private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState(); + @Unique + private static final BlockState VOID_AIR_BLOCKSTATE = Blocks.VOID_AIR.defaultBlockState(); + + @Unique + private int minSection; + + @Unique + private int maxSection; + + @Unique + private boolean debug; + + @Unique + private BlockState defaultBlockState; + + public LevelChunkMixin(ChunkPos chunkPos, UpgradeData upgradeData, LevelHeightAccessor levelHeightAccessor, + Registry registry, long l, LevelChunkSection[] levelChunkSections, BlendingData blendingData) { + super(chunkPos, upgradeData, levelHeightAccessor, registry, l, levelChunkSections, blendingData); + } + + /** + * Initialises the min/max section + */ + @Inject( + method = "(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/chunk/UpgradeData;Lnet/minecraft/world/ticks/LevelChunkTicks;Lnet/minecraft/world/ticks/LevelChunkTicks;J[Lnet/minecraft/world/level/chunk/LevelChunkSection;Lnet/minecraft/world/level/chunk/LevelChunk$PostLoadProcessor;Lnet/minecraft/world/level/levelgen/blending/BlendingData;)V", + at = @At("TAIL") + ) + public void onConstruct(Level level, ChunkPos chunkPos, UpgradeData upgradeData, LevelChunkTicks levelChunkTicks, LevelChunkTicks levelChunkTicks2, long l, LevelChunkSection[] levelChunkSections, LevelChunk.PostLoadProcessor postLoadProcessor, BlendingData blendingData, CallbackInfo ci) { + this.minSection = WorldUtil.getMinSection(level); + this.maxSection = WorldUtil.getMaxSection(level); + + final boolean empty = ((Object)this instanceof EmptyLevelChunk); + this.debug = !empty && this.level.isDebug(); + this.defaultBlockState = empty ? VOID_AIR_BLOCKSTATE : AIR_BLOCKSTATE; + } + + /** + * @reason Route to optimized getBlock + * @author Spottedleaf + */ + @Overwrite + public BlockState getBlockState(final BlockPos pos) { + return this.getBlock(pos.getX(), pos.getY(), pos.getZ()); + } + + @Unique + private BlockState getBlockDebug(final int x, final int y, final int z) { + if (y == 60) { + return Blocks.BARRIER.defaultBlockState(); + } + + if (y == 70) { + final BlockState ret = DebugLevelSource.getBlockStateFor(x, z); + return ret == null ? AIR_BLOCKSTATE : ret; + } + + return AIR_BLOCKSTATE; + } + + @Override + public BlockState getBlock(final int x, final int y, final int z) { + if (this.debug) { + return this.getBlockDebug(x, y, z); + } + + final int sectionY = (y >> 4) - this.minSection; + + final LevelChunkSection[] sections = this.sections; + if (sectionY < 0 || sectionY >= sections.length) { + return this.defaultBlockState; + } + + final LevelChunkSection section = sections[sectionY]; + + // note: when empty hasOnlyAir() is true, so we always fall to defaultBlockState + if (!section.hasOnlyAir()) { + final int index = (x & 15) | ((z & 15) << 4) | ((y & 15) << (4+4)); + return section.states.get(index); + } + + return this.defaultBlockState; + } + + /** + * @reason Replace with more optimised version + * @author Spottedleaf + */ + @Overwrite + public FluidState getFluidState(final int x, final int y, final int z) { + final int sectionY = (y >> 4) - this.minSection; + + final LevelChunkSection[] sections = this.sections; + if (sectionY < 0 || sectionY >= sections.length) { + return AIR_FLUIDSTATE; + } + + final LevelChunkSection section = sections[sectionY]; + + if (!section.hasOnlyAir()) { + final int index = (x & 15) | ((z & 15) << 4) | ((y & 15) << (4+4)); + return section.states.get(index).getFluidState(); + } + + return AIR_FLUIDSTATE; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ArmorStandMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ArmorStandMixin.java new file mode 100644 index 0000000..4130bdc --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ArmorStandMixin.java @@ -0,0 +1,42 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.decoration.ArmorStand; +import net.minecraft.world.entity.vehicle.AbstractMinecart; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import java.util.List; +import java.util.function.Predicate; + +@Mixin(ArmorStand.class) +public abstract class ArmorStandMixin extends LivingEntity { + + @Shadow + @Final + private static Predicate RIDABLE_MINECARTS; + + protected ArmorStandMixin(EntityType entityType, Level level) { + super(entityType, level); + } + + /** + * @reason Optimise this method by making it use the class lookup + * @author Spottedleaf + */ + @Overwrite + public void pushEntities() { + final List nearby = this.level().getEntitiesOfClass(AbstractMinecart.class, this.getBoundingBox(), RIDABLE_MINECARTS); + + for (int i = 0, len = nearby.size(); i < len; ++i) { + final AbstractMinecart minecart = nearby.get(i); + if (this.distanceToSqr(minecart) <= 0.2) { + minecart.push(this); + } + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ArrayVoxelShapeMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ArrayVoxelShapeMixin.java new file mode 100644 index 0000000..b24c25e --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ArrayVoxelShapeMixin.java @@ -0,0 +1,30 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import net.minecraft.world.phys.shapes.ArrayVoxelShape; +import net.minecraft.world.phys.shapes.DiscreteVoxelShape; +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; + +@Mixin(ArrayVoxelShape.class) +public abstract class ArrayVoxelShapeMixin { + + /** + * @reason Hook into the root constructor to pass along init data to superclass. + * @author Spottedleaf + */ + @Inject( + method = "(Lnet/minecraft/world/phys/shapes/DiscreteVoxelShape;Lit/unimi/dsi/fastutil/doubles/DoubleList;Lit/unimi/dsi/fastutil/doubles/DoubleList;Lit/unimi/dsi/fastutil/doubles/DoubleList;)V", + at = @At( + value = "RETURN" + ) + ) + private void initState(final DiscreteVoxelShape discreteVoxelShape, + final DoubleList xList, final DoubleList yList, final DoubleList zList, + final CallbackInfo ci) { + ((CollisionVoxelShape)this).initCache(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/BlockMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/BlockMixin.java new file mode 100644 index 0000000..f947531 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/BlockMixin.java @@ -0,0 +1,20 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +@Mixin(Block.class) +public abstract class BlockMixin { + + /** + * @reason Replace with an implementation that does not use join AND one that caches the result + * @author Spottedleaf + */ + @Overwrite + public static boolean isShapeFullBlock(final VoxelShape shape) { + return ((CollisionVoxelShape)shape).isFullBlock(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/BlockStateBaseMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/BlockStateBaseMixin.java new file mode 100644 index 0000000..e43c085 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/BlockStateBaseMixin.java @@ -0,0 +1,168 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState; +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; +import com.google.common.collect.ImmutableMap; +import com.mojang.serialization.MapCodec; +import it.unimi.dsi.fastutil.HashCommon; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateHolder; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.concurrent.atomic.AtomicInteger; + +@Mixin(BlockBehaviour.BlockStateBase.class) +public abstract class BlockStateBaseMixin extends StateHolder implements CollisionBlockState { + + @Unique + private static final int RANDOM_OFFSET = 704237939; + + @Unique + private static final Direction[] DIRECTIONS_CACHED = Direction.values(); + + @Unique + private static final AtomicInteger ID_GENERATOR = new AtomicInteger(RANDOM_OFFSET); + + @Unique + private int id1, id2; + + @Shadow + protected BlockBehaviour.BlockStateBase.Cache cache; + + @Shadow + public abstract VoxelShape getCollisionShape(BlockGetter blockGetter, BlockPos blockPos, CollisionContext collisionContext); + + + + protected BlockStateBaseMixin(Block object, ImmutableMap, Comparable> immutableMap, MapCodec mapCodec) { + super(object, immutableMap, mapCodec); + } + + + @Unique + private boolean occludesFullBlock; + + @Unique + private boolean emptyCollisionShape; + + @Unique + private VoxelShape constantCollisionShape; + + @Unique + private AABB constantAABBCollision; + + @Unique + private static void initCaches(final VoxelShape shape) { + ((CollisionVoxelShape)shape).isFullBlock(); + ((CollisionVoxelShape)shape).occludesFullBlock(); + shape.toAabbs(); + if (!shape.isEmpty()) { + shape.bounds(); + } + } + + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void init(final CallbackInfo ci) { + // note: murmurHash3 has an inverse, so the field is still unique + this.id1 = HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement()); + this.id2 = HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement()); + } + + /** + * @reason Init collision state only after cache is set up + * @author Spottedleaf + */ + @Inject( + method = "initCache", + at = @At( + value = "RETURN" + ) + ) + private void initCollisionState(final CallbackInfo ci) { + if (this.cache != null) { + final VoxelShape collisionShape = this.cache.collisionShape; + try { + this.constantCollisionShape = this.getCollisionShape(null, null, null); + this.constantAABBCollision = this.constantCollisionShape == null ? null : ((CollisionVoxelShape)this.constantCollisionShape).getSingleAABBRepresentation(); + } catch (final Throwable throwable) { + this.constantCollisionShape = null; + this.constantAABBCollision = null; + } + this.occludesFullBlock = ((CollisionVoxelShape)collisionShape).occludesFullBlock(); + this.emptyCollisionShape = collisionShape.isEmpty(); + // init caches + initCaches(collisionShape); + if (collisionShape != Shapes.empty() && collisionShape != Shapes.block()) { + for (final Direction direction : DIRECTIONS_CACHED) { + // initialise the directional face shape cache as well + final VoxelShape shape = Shapes.getFaceShape(collisionShape, direction); + initCaches(shape); + } + } + if (this.cache.occlusionShapes != null) { + for (final VoxelShape shape : this.cache.occlusionShapes) { + initCaches(shape); + } + } + } else { + this.occludesFullBlock = false; + this.emptyCollisionShape = false; + this.constantCollisionShape = null; + this.constantAABBCollision = null; + } + } + + @Override + public final boolean hasCache() { + return this.cache != null; + } + + @Override + public final boolean occludesFullBlock() { + return this.occludesFullBlock; + } + + @Override + public final boolean emptyCollisionShape() { + return this.emptyCollisionShape; + } + + @Override + public final int uniqueId1() { + return this.id1; + } + + @Override + public final int uniqueId2() { + return this.id2; + } + + @Override + public final VoxelShape getConstantCollisionShape() { + return this.constantCollisionShape; + } + + @Override + public final AABB getConstantCollisionAABB() { + return this.constantAABBCollision; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/CollisionGetterMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/CollisionGetterMixin.java new file mode 100644 index 0000000..fc91daf --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/CollisionGetterMixin.java @@ -0,0 +1,115 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.CollisionUtil; +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.CollisionGetter; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import java.util.Optional; + +@Mixin(CollisionGetter.class) +public interface CollisionGetterMixin extends BlockGetter { + + @Shadow + @Nullable + BlockGetter getChunkForCollisions(final int chunkX, final int chunkZ); + + /** + * @reason Route to faster logic + * @author Spottedleaf + */ + @Overwrite + default Optional findSupportingBlock(final Entity entity, final AABB aabb) { + final int minBlockX = Mth.floor(aabb.minX - CollisionUtil.COLLISION_EPSILON) - 1; + final int maxBlockX = Mth.floor(aabb.maxX + CollisionUtil.COLLISION_EPSILON) + 1; + + final int minBlockY = Mth.floor(aabb.minY - CollisionUtil.COLLISION_EPSILON) - 1; + final int maxBlockY = Mth.floor(aabb.maxY + CollisionUtil.COLLISION_EPSILON) + 1; + + final int minBlockZ = Mth.floor(aabb.minZ - CollisionUtil.COLLISION_EPSILON) - 1; + final int maxBlockZ = Mth.floor(aabb.maxZ + CollisionUtil.COLLISION_EPSILON) + 1; + + CollisionUtil.LazyEntityCollisionContext collisionContext = null; + + final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + BlockPos selected = null; + double selectedDistance = Double.MAX_VALUE; + + final Vec3 entityPos = entity.position(); + + for (int currZ = minBlockZ; currZ <= maxBlockZ; ++currZ) { + for (int currX = minBlockX; currX <= maxBlockX; ++currX) { + pos.set(currX, 0, currZ); + final BlockGetter chunk = this.getChunkForCollisions(currX >> 4, currZ >> 4); + if (chunk == null) { + continue; + } + for (int currY = minBlockY; currY <= maxBlockY; ++currY) { + int edgeCount = ((currX == minBlockX || currX == maxBlockX) ? 1 : 0) + + ((currY == minBlockY || currY == maxBlockY) ? 1 : 0) + + ((currZ == minBlockZ || currZ == maxBlockZ) ? 1 : 0); + if (edgeCount == 3) { + continue; + } + + pos.setY(currY); + + final double distance = pos.distToCenterSqr(entityPos); + if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(pos) >= 0)) { + continue; + } + + final BlockState state = chunk.getBlockState(pos); + if (state.isAir()) { + continue; + } + + if ((edgeCount != 1 || state.hasLargeCollisionShape()) && (edgeCount != 2 || state.getBlock() == Blocks.MOVING_PISTON)) { + if (collisionContext == null) { + collisionContext = new CollisionUtil.LazyEntityCollisionContext(entity); + } + final VoxelShape blockCollision = state.getCollisionShape(chunk, pos, collisionContext); + if (blockCollision.isEmpty()) { + continue; + } + + AABB singleAABB = ((CollisionVoxelShape)blockCollision).getSingleAABBRepresentation(); + if (singleAABB != null) { + singleAABB = singleAABB.move((double)currX, (double)currY, (double)currZ); + if (!CollisionUtil.voxelShapeIntersect(aabb, singleAABB)) { + continue; + } + + selected = pos.immutable(); + selectedDistance = distance; + continue; + } + + final VoxelShape blockCollisionOffset = blockCollision.move((double)currX, (double)currY, (double)currZ); + + if (!CollisionUtil.voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) { + continue; + } + + selected = pos.immutable(); + selectedDistance = distance; + continue; + } + } + } + } + + return Optional.ofNullable(selected); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/CubeVoxelShapeMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/CubeVoxelShapeMixin.java new file mode 100644 index 0000000..b82e092 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/CubeVoxelShapeMixin.java @@ -0,0 +1,27 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; +import net.minecraft.world.phys.shapes.CubeVoxelShape; +import net.minecraft.world.phys.shapes.DiscreteVoxelShape; +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; + +@Mixin(CubeVoxelShape.class) +public abstract class CubeVoxelShapeMixin { + + /** + * @reason Hook into the root constructor to pass along init data to superclass. + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void initState(final DiscreteVoxelShape discreteVoxelShape, final CallbackInfo ci) { + ((CollisionVoxelShape)this).initCache(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/DirectionMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/DirectionMixin.java new file mode 100644 index 0000000..c876d19 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/DirectionMixin.java @@ -0,0 +1,159 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.util.CollisionDirection; +import it.unimi.dsi.fastutil.HashCommon; +import net.minecraft.core.Direction; +import net.minecraft.core.Vec3i; +import org.joml.Quaternionf; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Direction.class) +public abstract class DirectionMixin implements CollisionDirection { + + @Unique + private static final int RANDOM_OFFSET = 2017601568; + + @Shadow + @Final + private static Direction[] VALUES; + + @Shadow + @Final + private Vec3i normal; + + @Shadow + @Final + private int oppositeIndex; + + @Shadow + public static Direction from3DDataValue(int i) { + return null; + } + + @Unique + private Direction opposite; + + @Unique + private Quaternionf rotation; + + @Unique + private int id = HashCommon.murmurHash3(((Enum)(Object)this).ordinal() + 1); + + @Unique + private int stepX; + + @Unique + private int stepY; + + @Unique + private int stepZ; + + /** + * @reason Initialise caches before class init is done. + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private static void initCaches(final CallbackInfo ci) { + for (final Direction direction : VALUES) { + ((DirectionMixin)(Object)direction).opposite = from3DDataValue(((DirectionMixin)(Object)direction).oppositeIndex); + ((DirectionMixin)(Object)direction).rotation = ((DirectionMixin)(Object)direction).getRotationUncached(); + ((DirectionMixin)(Object)direction).id = HashCommon.murmurHash3(direction.ordinal() + RANDOM_OFFSET); + ((DirectionMixin)(Object)direction).stepX = ((DirectionMixin)(Object)direction).normal.getX(); + ((DirectionMixin)(Object)direction).stepY = ((DirectionMixin)(Object)direction).normal.getY(); + ((DirectionMixin)(Object)direction).stepZ = ((DirectionMixin)(Object)direction).normal.getZ(); + } + } + + /** + * @reason Use simple field access + * @author Spottedleaf + */ + @Overwrite + public Direction getOpposite() { + return this.opposite; + } + + @Unique + private Quaternionf getRotationUncached() { + switch ((Direction)(Object)this) { + case DOWN: { + return new Quaternionf().rotationX(3.1415927F); + } + case UP: { + return new Quaternionf(); + } + case NORTH: { + return new Quaternionf().rotationXYZ(1.5707964F, 0.0F, 3.1415927F); + } + case SOUTH: { + return new Quaternionf().rotationX(1.5707964F); + } + case WEST: { + return new Quaternionf().rotationXYZ(1.5707964F, 0.0F, 1.5707964F); + } + case EAST: { + return new Quaternionf().rotationXYZ(1.5707964F, 0.0F, -1.5707964F); + } + default: { + throw new IllegalStateException(); + } + } + } + + /** + * @reason Use clone of cache instead of computing the rotation + * @author Spottedleaf + */ + @Overwrite + public Quaternionf getRotation() { + try { + return (Quaternionf)this.rotation.clone(); + } catch (final CloneNotSupportedException ex) { + throw new InternalError(ex); + } + } + + /** + * @reason Avoid extra memory indirection + * @author Spottedleaf + */ + @Overwrite + public int getStepX() { + return this.stepX; + } + + /** + * @reason Avoid extra memory indirection + * @author Spottedleaf + */ + @Overwrite + public int getStepY() { + return this.stepY; + } + + /** + * @reason Avoid extra memory indirection + * @author Spottedleaf + */ + @Overwrite + public int getStepZ() { + return this.stepZ; + } + + @Override + public int uniqueId() { + return this.id; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/DiscreteVoxelShapeMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/DiscreteVoxelShapeMixin.java new file mode 100644 index 0000000..3df8aac --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/DiscreteVoxelShapeMixin.java @@ -0,0 +1,65 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData; +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape; +import net.minecraft.core.Direction; +import net.minecraft.world.phys.shapes.DiscreteVoxelShape; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(DiscreteVoxelShape.class) +public abstract class DiscreteVoxelShapeMixin implements CollisionDiscreteVoxelShape { + + // ignore race conditions here: the shape is static, so it doesn't matter + private CachedShapeData cachedShapeData; + + @Override + public final CachedShapeData getOrCreateCachedShapeData() { + if (this.cachedShapeData != null) { + return this.cachedShapeData; + } + + final DiscreteVoxelShape discreteVoxelShape = (DiscreteVoxelShape)(Object)this; + + final int sizeX = discreteVoxelShape.getXSize(); + final int sizeY = discreteVoxelShape.getYSize(); + final int sizeZ = discreteVoxelShape.getZSize(); + + final int maxIndex = sizeX * sizeY * sizeZ; // exclusive + + final int longsRequired = (maxIndex + (Long.SIZE - 1)) >>> 6; + final long[] voxelSet = new long[longsRequired]; + + final boolean isEmpty = discreteVoxelShape.isEmpty(); + if (!isEmpty) { + for (int x = 0; x < sizeX; ++x) { + for (int y = 0; y < sizeY; ++y) { + for (int z = 0; z < sizeZ; ++z) { + if (discreteVoxelShape.isFull(x, y, z)) { + // index = z + y*size_z + x*(size_z*size_y) + final int index = z + y * sizeZ + x * sizeZ * sizeY; + + voxelSet[index >>> 6] |= 1L << index; + } + } + } + } + } + + final boolean hasSingleAABB = sizeX == 1 && sizeY == 1 && sizeZ == 1 && !isEmpty && discreteVoxelShape.isFull(0, 0, 0); + + final int minFullX = discreteVoxelShape.firstFull(Direction.Axis.X); + final int minFullY = discreteVoxelShape.firstFull(Direction.Axis.Y); + final int minFullZ = discreteVoxelShape.firstFull(Direction.Axis.Z); + + final int maxFullX = discreteVoxelShape.lastFull(Direction.Axis.X); + final int maxFullY = discreteVoxelShape.lastFull(Direction.Axis.Y); + final int maxFullZ = discreteVoxelShape.lastFull(Direction.Axis.Z); + + return this.cachedShapeData = new CachedShapeData( + sizeX, sizeY, sizeZ, voxelSet, + minFullX, minFullY, minFullZ, + maxFullX, maxFullY, maxFullZ, + isEmpty, hasSingleAABB + ); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/EntityGetterMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/EntityGetterMixin.java new file mode 100644 index 0000000..ab79319 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/EntityGetterMixin.java @@ -0,0 +1,110 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.CollisionUtil; +import ca.spottedleaf.moonrise.patches.collisions.entity.CollisionEntity; +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; +import ca.spottedleaf.moonrise.patches.collisions.world.CollisionEntityGetter; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.EntityGetter; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +@Mixin(EntityGetter.class) +public interface EntityGetterMixin extends CollisionEntityGetter { + + @Shadow + List getEntities(final Entity entity, final AABB box, final Predicate predicate); + + @Shadow + List getEntities(final Entity entity, final AABB box); + + /** + * @reason Route to faster lookup, and fix behavioral issues + * See {@link CollisionUtil#getEntityHardCollisions} for expected behavior + * @author Spottedleaf + */ + @Overwrite + default List getEntityCollisions(final Entity entity, AABB box) { + // first behavior change is to correctly check for empty AABB + if (CollisionUtil.isEmpty(box)) { + // reduce indirection by always returning type with same class + return new ArrayList<>(); + } + + // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with. + // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems + // specifically with boat collisions. + box = box.inflate(-CollisionUtil.COLLISION_EPSILON, -CollisionUtil.COLLISION_EPSILON, -CollisionUtil.COLLISION_EPSILON); + + final List entities; + if (entity != null && ((CollisionEntity)entity).isHardColliding()) { + entities = this.getEntities(entity, box, null); + } else { + entities = this.getHardCollidingEntities(entity, box, null); + } + + final List ret = new ArrayList<>(Math.min(25, entities.size())); + + for (int i = 0, len = entities.size(); i < len; ++i) { + final Entity otherEntity = entities.get(i); + + if (otherEntity.isSpectator()) { + continue; + } + + if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) { + ret.add(Shapes.create(otherEntity.getBoundingBox())); + } + } + + return ret; + } + + @Override + default List getHardCollidingEntities(final Entity entity, final AABB box, final Predicate predicate) { + return this.getEntities(entity, box, predicate); + } + + /** + * @reason Use faster intersection checks + * @author Spottedleaf + */ + @Overwrite + default boolean isUnobstructed(final Entity entity, final VoxelShape voxel) { + if (voxel.isEmpty()) { + return false; + } + + final AABB singleAABB = ((CollisionVoxelShape)voxel).getSingleAABBRepresentation(); + final List entities = this.getEntities( + entity, + singleAABB == null ? voxel.bounds() : singleAABB.inflate(-CollisionUtil.COLLISION_EPSILON, -CollisionUtil.COLLISION_EPSILON, -CollisionUtil.COLLISION_EPSILON) + ); + + for (int i = 0, len = entities.size(); i < len; ++i) { + final Entity otherEntity = entities.get(i); + + if (otherEntity.isRemoved() || !otherEntity.blocksBuilding || (entity != null && otherEntity.isPassengerOfSameVehicle(entity))) { + continue; + } + + if (singleAABB == null) { + final AABB entityBB = otherEntity.getBoundingBox(); + if (CollisionUtil.isEmpty(entityBB) || !CollisionUtil.voxelShapeIntersectNoEmpty(voxel, entityBB)) { + continue; + } + } + + return false; + } + + return true; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/EntityMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/EntityMixin.java new file mode 100644 index 0000000..4ead2b4 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/EntityMixin.java @@ -0,0 +1,378 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.patches.collisions.CollisionUtil; +import ca.spottedleaf.moonrise.patches.collisions.entity.CollisionEntity; +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; +import ca.spottedleaf.moonrise.patches.collisions.util.EmptyStreamForMoveCall; +import net.minecraft.CrashReport; +import net.minecraft.CrashReportCategory; +import net.minecraft.ReportedException; +import net.minecraft.core.BlockPos; +import net.minecraft.tags.BlockTags; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +@Mixin(Entity.class) +public abstract class EntityMixin implements CollisionEntity { + + @Shadow + private Level level; + + @Shadow + public abstract AABB getBoundingBox(); + + @Shadow + public abstract void move(MoverType moverType, Vec3 vec3); + + @Shadow + public abstract float maxUpStep(); + + @Shadow + private boolean onGround; + + @Shadow + public boolean noPhysics; + + @Shadow + private EntityDimensions dimensions; + + @Shadow + public abstract Vec3 getEyePosition(); + + @Shadow + public abstract Vec3 getEyePosition(float f); + + @Shadow + public abstract Vec3 getViewVector(float f); + + @Shadow + private int remainingFireTicks; + + @Shadow + public abstract void setRemainingFireTicks(int i); + + @Shadow + protected abstract int getFireImmuneTicks(); + + @Shadow + public boolean wasOnFire; + @Shadow + public boolean isInPowderSnow; + + @Shadow + public abstract boolean isInWaterRainOrBubble(); + + @Shadow + protected abstract void playEntityOnFireExtinguishedSound(); + + @Shadow + protected abstract void onInsideBlock(BlockState blockState); + + @Unique + private final boolean isHardColliding = this.isHardCollidingUncached(); + + @Override + public final boolean isHardColliding() { + return this.isHardColliding; + } + + /** + * @author Spottedleaf + * @reason Optimise entire method + */ + @Overwrite + private Vec3 collide(final Vec3 movement) { + final boolean xZero = movement.x == 0.0; + final boolean yZero = movement.y == 0.0; + final boolean zZero = movement.z == 0.0; + if (xZero & yZero & zZero) { + return movement; + } + + final Level world = this.level; + final AABB currBoundingBox = this.getBoundingBox(); + + if (CollisionUtil.isEmpty(currBoundingBox)) { + return movement; + } + + final List potentialCollisionsBB = new ArrayList<>(); + final List potentialCollisionsVoxel = new ArrayList<>(); + final double stepHeight = (double)this.maxUpStep(); + final AABB collisionBox; + final boolean onGround = this.onGround; + + if (xZero & zZero) { + if (movement.y > 0.0) { + collisionBox = CollisionUtil.cutUpwards(currBoundingBox, movement.y); + } else { + collisionBox = CollisionUtil.cutDownwards(currBoundingBox, movement.y); + } + } else { + // note: xZero == false or zZero == false + if (stepHeight > 0.0 && (onGround || (movement.y < 0.0))) { + // don't bother getting the collisions if we don't need them. + if (movement.y <= 0.0) { + collisionBox = CollisionUtil.expandUpwards(currBoundingBox.expandTowards(movement.x, movement.y, movement.z), stepHeight); + } else { + collisionBox = currBoundingBox.expandTowards(movement.x, Math.max(stepHeight, movement.y), movement.z); + } + } else { + collisionBox = currBoundingBox.expandTowards(movement.x, movement.y, movement.z); + } + } + + CollisionUtil.getCollisions( + world, (Entity)(Object)this, collisionBox, potentialCollisionsVoxel, potentialCollisionsBB, + (0), + null, null + ); + + if (CollisionUtil.isCollidingWithBorderEdge(world.getWorldBorder(), collisionBox)) { + potentialCollisionsVoxel.add(world.getWorldBorder().getCollisionShape()); + } + + if (potentialCollisionsVoxel.isEmpty() && potentialCollisionsBB.isEmpty()) { + return movement; + } + + final Vec3 limitedMoveVector = CollisionUtil.performCollisions(movement, currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB); + + if (stepHeight > 0.0 + && (onGround || (limitedMoveVector.y != movement.y && movement.y < 0.0)) + && (limitedMoveVector.x != movement.x || limitedMoveVector.z != movement.z)) { + Vec3 vec3d2 = CollisionUtil.performCollisions(new Vec3(movement.x, stepHeight, movement.z), currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB); + final Vec3 vec3d3 = CollisionUtil.performCollisions(new Vec3(0.0, stepHeight, 0.0), currBoundingBox.expandTowards(movement.x, 0.0, movement.z), potentialCollisionsVoxel, potentialCollisionsBB); + + if (vec3d3.y < stepHeight) { + final Vec3 vec3d4 = CollisionUtil.performCollisions(new Vec3(movement.x, 0.0D, movement.z), currBoundingBox.move(vec3d3), potentialCollisionsVoxel, potentialCollisionsBB).add(vec3d3); + + if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) { + vec3d2 = vec3d4; + } + } + + if (vec3d2.horizontalDistanceSqr() > limitedMoveVector.horizontalDistanceSqr()) { + return vec3d2.add(CollisionUtil.performCollisions(new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), currBoundingBox.move(vec3d2), potentialCollisionsVoxel, potentialCollisionsBB)); + } + + return limitedMoveVector; + } else { + return limitedMoveVector; + } + } + + /** + * @reason Redirects the vanilla move() call fire checks to short circuit to false, as we want to avoid the rather + * expensive stream usage here. The below method then reinserts the logic, without using streams. + * @author Spottedleaf + */ + @Redirect( + method = "move", + at = @At( + target = "Lnet/minecraft/world/level/Level;getBlockStatesIfLoaded(Lnet/minecraft/world/phys/AABB;)Ljava/util/stream/Stream;", + value = "INVOKE", + ordinal = 0 + ) + ) + public Stream shortCircuitStreamLogic(final Level level, final AABB box) { + return EmptyStreamForMoveCall.INSTANCE; + } + + /** + * @reason Merge fire and block checking logic, so that we can combine the chunk loaded check and the chunk cache + * @author Spottedleaf + */ + @Redirect( + method = "move", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/Entity;tryCheckInsideBlocks()V" + ) + ) + public void checkInsideBlocks(final Entity instance) { + final AABB boundingBox = this.getBoundingBox(); + final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos(); + + final int minX = Mth.floor(boundingBox.minX - 1.0E-6); + final int minY = Mth.floor(boundingBox.minY - 1.0E-6); + final int minZ = Mth.floor(boundingBox.minZ - 1.0E-6); + final int maxX = Mth.floor(boundingBox.maxX + 1.0E-6); + final int maxY = Mth.floor(boundingBox.maxY + 1.0E-6); + final int maxZ = Mth.floor(boundingBox.maxZ + 1.0E-6); + + long lastChunkKey = ChunkPos.INVALID_CHUNK_POS; + LevelChunk lastChunk = null; + + boolean noneMatch = true; + boolean isLoaded = true; + + fire_search: + for (int fz = minZ; fz <= maxZ; ++fz) { + tempPos.setZ(fz); + for (int fx = minX; fx <= maxX; ++fx) { + final int newChunkX = fx >> 4; + final int newChunkZ = fz >> 4; + final LevelChunk chunk = lastChunkKey == (lastChunkKey = CoordinateUtils.getChunkKey(newChunkX, newChunkZ)) ? + lastChunk : (lastChunk = (LevelChunk)this.level.getChunk(fx >> 4, fz >> 4, ChunkStatus.FULL, false)); + if (chunk == null) { + // would have returned empty stream + noneMatch = true; + isLoaded = false; + break fire_search; + } + tempPos.setX(fx); + for (int fy = minY; fy <= maxY; ++fy) { + tempPos.setY(fy); + final BlockState state = chunk.getBlockState(tempPos); + if (state.is(BlockTags.FIRE) || state.is(Blocks.LAVA)) { + noneMatch = false; + // need to continue to check for loaded chunks + } + } + } + } + + if (isLoaded) { + final int minX2 = Mth.floor(boundingBox.minX + CollisionUtil.COLLISION_EPSILON); + final int minY2 = Mth.floor(boundingBox.minY + CollisionUtil.COLLISION_EPSILON); + final int minZ2 = Mth.floor(boundingBox.minZ + CollisionUtil.COLLISION_EPSILON); + final int maxX2 = Mth.floor(boundingBox.maxX - CollisionUtil.COLLISION_EPSILON); + final int maxY2 = Mth.floor(boundingBox.maxY - CollisionUtil.COLLISION_EPSILON); + final int maxZ2 = Mth.floor(boundingBox.maxZ - CollisionUtil.COLLISION_EPSILON); + + for (int fx = minX2; fx <= maxX2; ++fx) { + tempPos.setX(fx); + for (int fy = minY2; fy <= maxY2; ++fy) { + tempPos.setY(fy); + for (int fz = minZ2; fz <= maxZ2; ++fz) { + tempPos.setZ(fz); + + final int newChunkX = fx >> 4; + final int newChunkZ = fz >> 4; + final LevelChunk chunk = lastChunkKey == (lastChunkKey = CoordinateUtils.getChunkKey(newChunkX, newChunkZ)) ? + lastChunk : (lastChunk = this.level.getChunk(fx >> 4, fz >> 4)); + final BlockState blockState = chunk.getBlockState(tempPos); + + if (blockState.isAir()) { + continue; + } + + try { + blockState.entityInside(this.level, tempPos, (Entity)(Object)this); + this.onInsideBlock(blockState); + } catch (Throwable var12) { + CrashReport crashReport = CrashReport.forThrowable(var12, "Colliding entity with block"); + CrashReportCategory crashReportCategory = crashReport.addCategory("Block being collided with"); + CrashReportCategory.populateBlockDetails(crashReportCategory, this.level, tempPos, blockState); + throw new ReportedException(crashReport); + } + } + } + } + } + + // to preserve order with tryCheckInsideBlocks, we need to move the fire logic _after_ + if (noneMatch) { + if (this.remainingFireTicks <= 0) { + this.setRemainingFireTicks(-this.getFireImmuneTicks()); + } + + if (this.wasOnFire && (this.isInPowderSnow || this.isInWaterRainOrBubble())) { + this.playEntityOnFireExtinguishedSound(); + } + } + } + + /** + * @author Spottedleaf + * @reason Optimise entire method - removed the stream + use optimised collisions + */ + @Overwrite + public boolean isInWall() { + if (this.noPhysics) { + return false; + } + + final float reducedWith = this.dimensions.width * 0.8F; + final AABB box = AABB.ofSize(this.getEyePosition(), reducedWith, 1.0E-6D, reducedWith); + + if (CollisionUtil.isEmpty(box)) { + return false; + } + + final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos(); + + final int minX = Mth.floor(box.minX); + final int minY = Mth.floor(box.minY); + final int minZ = Mth.floor(box.minZ); + final int maxX = Mth.floor(box.maxX); + final int maxY = Mth.floor(box.maxY); + final int maxZ = Mth.floor(box.maxZ); + + long lastChunkKey = ChunkPos.INVALID_CHUNK_POS; + LevelChunk lastChunk = null; + for (int fz = minZ; fz <= maxZ; ++fz) { + tempPos.setZ(fz); + for (int fx = minX; fx <= maxX; ++fx) { + final int newChunkX = fx >> 4; + final int newChunkZ = fz >> 4; + final LevelChunk chunk = lastChunkKey == (lastChunkKey = CoordinateUtils.getChunkKey(newChunkX, newChunkZ)) ? + lastChunk : (lastChunk = this.level.getChunk(fx >> 4, fz >> 4)); + tempPos.setX(fx); + for (int fy = minY; fy <= maxY; ++fy) { + tempPos.setY(fy); + + final BlockState state = chunk.getBlockState(tempPos); + + if (state.isAir() || !state.isSuffocating(this.level, tempPos)) { + continue; + } + + // Yes, it does not use the Entity context stuff. + final VoxelShape collisionShape = state.getCollisionShape(this.level, tempPos); + + if (collisionShape.isEmpty()) { + continue; + } + + final AABB singleAABB = ((CollisionVoxelShape)collisionShape).getSingleAABBRepresentation(); + if (singleAABB != null) { + if (CollisionUtil.voxelShapeIntersect(box, singleAABB)) { + return true; + } + continue; + } + + if (CollisionUtil.voxelShapeIntersectNoEmpty(collisionShape, box)) { + return true; + } + continue; + } + } + } + + return false; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LevelChunkSectionMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LevelChunkSectionMixin.java new file mode 100644 index 0000000..1ff9ccb --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LevelChunkSectionMixin.java @@ -0,0 +1,76 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.CollisionUtil; +import ca.spottedleaf.moonrise.patches.collisions.world.BlockCounter; +import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevelChunkSection; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(LevelChunkSection.class) +public abstract class LevelChunkSectionMixin implements CollisionLevelChunkSection { + + @Shadow + @Final + public PalettedContainer states; + + @Shadow + private short nonEmptyBlockCount; + + @Shadow + private short tickingBlockCount; + + @Shadow + private short tickingFluidCount; + + + @Unique + private int specialCollidingBlocks; + + /** + * @reason Callback used to update the known collision data on block update. + * @author Spottedleaf + */ + @Inject( + method = "setBlockState(IIILnet/minecraft/world/level/block/state/BlockState;Z)Lnet/minecraft/world/level/block/state/BlockState;", + at = @At("RETURN") + ) + private void updateBlockCallback(final int x, final int y, final int z, final BlockState state, final boolean lock, + final CallbackInfoReturnable cir) { + if (CollisionUtil.isSpecialCollidingBlock(state)) { + ++this.specialCollidingBlocks; + } + if (CollisionUtil.isSpecialCollidingBlock(cir.getReturnValue())) { + --this.specialCollidingBlocks; + } + } + + /** + * @reason Insert known collision data counting + * @author Spottedleaf + */ + @Overwrite + public void recalcBlockCounts() { + final BlockCounter counter = new BlockCounter(); + + this.states.count(counter); + + this.nonEmptyBlockCount = (short)counter.nonEmptyBlockCount; + this.tickingBlockCount = (short)counter.tickingBlockCount; + this.tickingFluidCount = (short)counter.tickingFluidCount; + this.specialCollidingBlocks = (short)counter.specialCollidingBlocks; + } + + @Override + public final int getSpecialCollidingBlocks() { + return this.specialCollidingBlocks; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LevelMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LevelMixin.java new file mode 100644 index 0000000..6b5f55e --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LevelMixin.java @@ -0,0 +1,405 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.collisions.CollisionUtil; +import ca.spottedleaf.moonrise.patches.collisions.slices.EntityLookup; +import ca.spottedleaf.moonrise.patches.collisions.world.CollisionEntityGetter; +import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.Mth; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +@Mixin(Level.class) +public abstract class LevelMixin implements CollisionLevel, CollisionEntityGetter, LevelAccessor, AutoCloseable { + + @Shadow + public abstract ProfilerFiller getProfiler(); + + @Shadow + public abstract LevelChunk getChunk(int x, int z); + + + + @Unique + private final EntityLookup collisionLookup = new EntityLookup((Level)(Object)this); + + @Unique + private int minSection; + + @Unique + private int maxSection; + + @Override + public EntityLookup getCollisionLookup() { + return this.collisionLookup; + } + + @Override + public int getMinSectionMoonrise() { + return this.minSection; + } + + @Override + public int getMaxSectionMoonrise() { + return this.maxSection; + } + + /** + * @reason Init min/max section + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void init(final CallbackInfo ci) { + this.minSection = WorldUtil.getMinSection(this); + this.maxSection = WorldUtil.getMaxSection(this); + } + + /** + * @reason Route to faster lookup + * @author Spottedleaf + */ + @Overwrite + public List getEntities(final Entity entity, final AABB boundingBox, final Predicate predicate) { + this.getProfiler().incrementCounter("getEntities"); + final List ret = new ArrayList<>(); + + this.collisionLookup.getEntities(entity, boundingBox, ret, predicate); + + return ret; + } + + /** + * @reason Route to faster lookup + * @author Spottedleaf + */ + @Overwrite + public void getEntities(final EntityTypeTest entityTypeTest, + final AABB boundingBox, final Predicate predicate, + final List into, final int maxCount) { + this.getProfiler().incrementCounter("getEntities"); + if (entityTypeTest instanceof EntityType byType) { + if (maxCount != Integer.MAX_VALUE) { + this.collisionLookup.getEntities(byType, boundingBox, into, predicate, maxCount); + return; + } else { + this.collisionLookup.getEntities(byType, boundingBox, into, predicate); + return; + } + } + + if (entityTypeTest == null) { + if (maxCount != Integer.MAX_VALUE) { + this.collisionLookup.getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate, maxCount); + return; + } else { + this.collisionLookup.getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate); + return; + } + } + + final Class base = entityTypeTest.getBaseClass(); + + final Predicate modifiedPredicate; + if (predicate == null) { + modifiedPredicate = (final T obj) -> { + return entityTypeTest.tryCast(obj) != null; + }; + } else { + modifiedPredicate = (final Entity obj) -> { + final T casted = entityTypeTest.tryCast(obj); + if (casted == null) { + return false; + } + + return predicate.test(casted); + }; + } + + if (base == null || base == Entity.class) { + if (maxCount != Integer.MAX_VALUE) { + this.collisionLookup.getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount); + return; + } else { + this.collisionLookup.getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate); + return; + } + } else { + if (maxCount != Integer.MAX_VALUE) { + this.collisionLookup.getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount); + return; + } else { + this.collisionLookup.getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate); + return; + } + } + } + + /** + * Route to faster lookup + * @author Spottedleaf + */ + @Override + public List getEntitiesOfClass(final Class entityClass, final AABB boundingBox, final Predicate predicate) { + this.getProfiler().incrementCounter("getEntities"); + final List ret = new ArrayList<>(); + + this.collisionLookup.getEntities(entityClass, null, boundingBox, ret, predicate); + + return ret; + } + + /** + * Route to faster lookup + * @author Spottedleaf + */ + @Override + public List getHardCollidingEntities(final Entity entity, final AABB box, final Predicate predicate) { + this.getProfiler().incrementCounter("getEntities"); + final List ret = new ArrayList<>(); + + this.collisionLookup.getHardCollidingEntities(entity, box, ret, predicate); + + return ret; + } + + /** + * Route to faster lookup. + * See {@link EntityGetterMixin#isUnobstructed(Entity, VoxelShape)} for expected behavior + * @author Spottedleaf + */ + @Override + public boolean isUnobstructed(final Entity entity) { + final AABB boundingBox = entity.getBoundingBox(); + if (CollisionUtil.isEmpty(boundingBox)) { + return false; + } + + final List entities = this.getEntities( + entity, + boundingBox.inflate(-CollisionUtil.COLLISION_EPSILON, -CollisionUtil.COLLISION_EPSILON, -CollisionUtil.COLLISION_EPSILON), + null + ); + + for (int i = 0, len = entities.size(); i < len; ++i) { + final Entity otherEntity = entities.get(i); + + if (otherEntity.isSpectator() || otherEntity.isRemoved() || !otherEntity.blocksBuilding || otherEntity.isPassengerOfSameVehicle(entity)) { + continue; + } + + return false; + } + + return true; + } + + + @Unique + private static BlockHitResult miss(final ClipContext clipContext) { + final Vec3 to = clipContext.getTo(); + final Vec3 from = clipContext.getFrom(); + + return BlockHitResult.miss(to, Direction.getNearest(from.x - to.x, from.y - to.y, from.z - to.z), BlockPos.containing(to.x, to.y, to.z)); + } + + @Unique + private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState(); + + @Unique + private static BlockHitResult fastClip(final Vec3 from, final Vec3 to, final Level level, + final ClipContext clipContext) { + final double adjX = CollisionUtil.COLLISION_EPSILON * (from.x - to.x); + final double adjY = CollisionUtil.COLLISION_EPSILON * (from.y - to.y); + final double adjZ = CollisionUtil.COLLISION_EPSILON * (from.z - to.z); + + if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) { + return miss(clipContext); + } + + final double toXAdj = to.x - adjX; + final double toYAdj = to.y - adjY; + final double toZAdj = to.z - adjZ; + final double fromXAdj = from.x + adjX; + final double fromYAdj = from.y + adjY; + final double fromZAdj = from.z + adjZ; + + int currX = Mth.floor(fromXAdj); + int currY = Mth.floor(fromYAdj); + int currZ = Mth.floor(fromZAdj); + + final BlockPos.MutableBlockPos currPos = new BlockPos.MutableBlockPos(); + + final double diffX = toXAdj - fromXAdj; + final double diffY = toYAdj - fromYAdj; + final double diffZ = toZAdj - fromZAdj; + + final double dxDouble = Math.signum(diffX); + final double dyDouble = Math.signum(diffY); + final double dzDouble = Math.signum(diffZ); + + final int dx = (int)dxDouble; + final int dy = (int)dyDouble; + final int dz = (int)dzDouble; + + final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX; + final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY; + final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ; + + double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj)); + double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj)); + double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj)); + + LevelChunkSection[] lastChunk = null; + PalettedContainer lastSection = null; + int lastChunkX = Integer.MIN_VALUE; + int lastChunkY = Integer.MIN_VALUE; + int lastChunkZ = Integer.MIN_VALUE; + + final int minSection = WorldUtil.getMinSection(level); + + for (;;) { + currPos.set(currX, currY, currZ); + + final int newChunkX = currX >> 4; + final int newChunkY = currY >> 4; + final int newChunkZ = currZ >> 4; + + final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); + final int chunkYDiff = newChunkY ^ lastChunkY; + + if ((chunkDiff | chunkYDiff) != 0) { + if (chunkDiff != 0) { + lastChunk = level.getChunk(newChunkX, newChunkZ).getSections(); + } + final int sectionY = newChunkY - minSection; + lastSection = sectionY >= 0 && sectionY < lastChunk.length ? lastChunk[sectionY].states : null; + + lastChunkX = newChunkX; + lastChunkY = newChunkY; + lastChunkZ = newChunkZ; + } + + final BlockState blockState; + if (lastSection != null && !(blockState = lastSection.get((currX & 15) | ((currZ & 15) << 4) | ((currY & 15) << (4+4)))).isAir()) { + final VoxelShape blockCollision = clipContext.getBlockShape(blockState, level, currPos); + + final BlockHitResult blockHit = blockCollision.isEmpty() ? null : level.clipWithInteractionOverride(from, to, currPos, blockCollision, blockState); + + final VoxelShape fluidCollision; + final FluidState fluidState; + if (clipContext.fluid != ClipContext.Fluid.NONE && (fluidState = blockState.getFluidState()) != AIR_FLUIDSTATE) { + fluidCollision = clipContext.getFluidShape(fluidState, level, currPos); + + final BlockHitResult fluidHit = fluidCollision.clip(from, to, currPos); + + if (fluidHit != null) { + if (blockHit == null) { + return fluidHit; + } + + return from.distanceToSqr(blockHit.getLocation()) <= from.distanceToSqr(fluidHit.getLocation()) ? blockHit : fluidHit; + } + } + + if (blockHit != null) { + return blockHit; + } + } // else: usually fall here + + if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) { + return miss(clipContext); + } + + // inc the smallest normalized coordinate + + if (normalizedCurrX < normalizedCurrY) { + if (normalizedCurrX < normalizedCurrZ) { + currX += dx; + normalizedCurrX += normalizedDiffX; + } else { + // x < y && x >= z <--> z < y && z <= x + currZ += dz; + normalizedCurrZ += normalizedDiffZ; + } + } else if (normalizedCurrY < normalizedCurrZ) { + // y <= x && y < z + currY += dy; + normalizedCurrY += normalizedDiffY; + } else { + // y <= x && z <= y <--> z <= y && z <= x + currZ += dz; + normalizedCurrZ += normalizedDiffZ; + } + } + } + + /** + * @reason Route to optimized call + * @author Spottedleaf + */ + @Override + public BlockHitResult clip(final ClipContext clipContext) { + // can only do this in this class, as not everything that implements BlockGetter can retrieve chunks + return fastClip(clipContext.getFrom(), clipContext.getTo(), (Level)(Object)this, clipContext); + } + + /** + * @reason Route to faster logic + * @author Spottedleaf + */ + @Override + public final boolean noCollision(final Entity entity, final AABB box) { + final int flags = entity == null ? (CollisionUtil.COLLISION_FLAG_CHECK_BORDER | CollisionUtil.COLLISION_FLAG_CHECK_ONLY) : CollisionUtil.COLLISION_FLAG_CHECK_ONLY; + if (CollisionUtil.getCollisionsForBlocksOrWorldBorder((Level)(Object)this, entity, box, null, null, flags, null)) { + return false; + } + + return !CollisionUtil.getEntityHardCollisions((Level)(Object)this, entity, box, null, flags, null); + } + + /** + * @reason Route to faster logic + * @author Spottedleaf + */ + @Override + public final boolean collidesWithSuffocatingBlock(final Entity entity, final AABB box) { + return CollisionUtil.getCollisionsForBlocksOrWorldBorder((Level)(Object)this, entity, box, null, null, + CollisionUtil.COLLISION_FLAG_CHECK_ONLY, + (final BlockState state, final BlockPos pos) -> { + return state.isSuffocating((Level)(Object)LevelMixin.this, pos); + } + ); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LiquidBlockRendererMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LiquidBlockRendererMixin.java new file mode 100644 index 0000000..ccc3bc0 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LiquidBlockRendererMixin.java @@ -0,0 +1,74 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.CollisionUtil; +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import net.minecraft.client.renderer.block.LiquidBlockRenderer; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.shapes.ArrayVoxelShape; +import net.minecraft.world.phys.shapes.BooleanOp; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +@Mixin(LiquidBlockRenderer.class) +public abstract class LiquidBlockRendererMixin { + + /** + * @reason Eliminate uncached extrusion of the water block height shape + * @author Spottedleaf + */ + @Overwrite + private static boolean isFaceOccludedByState(final BlockGetter world, final Direction direction, final float height, + final BlockPos pos, final BlockState state) { + if (!state.canOcclude()) { + return false; + } + + // check for created shape is empty + if (height < (float)CollisionUtil.COLLISION_EPSILON) { + return false; + } + + final boolean isOne = Math.abs(height - 1.0f) <= (float)CollisionUtil.COLLISION_EPSILON; + + final double heightDouble = (double)height; + final VoxelShape heightShape; + + // create extruded shape directly + if (isOne || direction == Direction.DOWN) { + // if height is one, then obviously it's a block + // otherwise, extrusion from DOWN will not use the height, in which case it is a block + heightShape = Shapes.block(); + } else if (direction == Direction.UP) { + // up is positive, so the first shape passed to blockOccudes must have 1.0 height + return false; + } else { + // the extrusion includes the height + heightShape = new ArrayVoxelShape( + Shapes.block().shape, + CollisionUtil.ZERO_ONE, + DoubleArrayList.wrap(new double[] { 0.0, heightDouble }), + CollisionUtil.ZERO_ONE + ); + } + + final VoxelShape stateShape = ((CollisionVoxelShape)state.getOcclusionShape(world, pos)).getFaceShapeClamped(direction.getOpposite()); + + if (stateShape.isEmpty()) { + // cannot occlude + return false; + } + + // fast check for box + if (heightShape == stateShape) { + return true; + } + + return !Shapes.joinIsNotEmpty(heightShape, stateShape, BooleanOp.ONLY_FIRST); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LivingEntityMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LivingEntityMixin.java new file mode 100644 index 0000000..b405fff --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LivingEntityMixin.java @@ -0,0 +1,58 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel; +import net.minecraft.world.entity.Attackable; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySelector; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import java.util.ArrayList; +import java.util.List; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityMixin extends Entity implements Attackable { + + @Shadow + protected abstract void doPush(Entity entity); + + public LivingEntityMixin(EntityType entityType, Level level) { + super(entityType, level); + } + + /** + * @reason Optimise this method + * @author Spottedleaf + */ + @Overwrite + public void pushEntities() { + if (this.level().isClientSide()) { + final List players = new ArrayList<>(); + ((CollisionLevel)this.level()).getCollisionLookup().getEntities(Player.class, this, this.getBoundingBox(), players, EntitySelector.pushableBy(this)); + for (int i = 0, len = players.size(); i < len; ++i) { + this.doPush(players.get(i)); + } + } else { + final List nearby = this.level().getEntities(this, this.getBoundingBox(), EntitySelector.pushableBy(this)); + + // only iterate ONCE + int nonPassengers = 0; + for (int i = 0, len = nearby.size(); i < len; ++i) { + final Entity entity = nearby.get(i); + nonPassengers += (entity.isPassenger() ? 1 : 0); + this.doPush(entity); + } + + int maxCramming; + if (nonPassengers != 0 && (maxCramming = this.level().getGameRules().getInt(GameRules.RULE_MAX_ENTITY_CRAMMING)) > 0 + && nonPassengers > (maxCramming - 1) && this.random.nextInt(4) == 0) { + this.hurt(this.damageSources().cramming(), 6.0F); + } + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ParticleMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ParticleMixin.java new file mode 100644 index 0000000..8fbc1b3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ParticleMixin.java @@ -0,0 +1,131 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.chunk_getblock.GetBlockChunk; +import ca.spottedleaf.moonrise.patches.collisions.CollisionUtil; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.particle.Particle; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; +import java.util.ArrayList; +import java.util.List; + +@Mixin(Particle.class) +public abstract class ParticleMixin { + + @Shadow + protected double x; + + @Shadow + protected double y; + + @Shadow + protected double z; + + @Shadow + @Final + protected ClientLevel level; + + + + /** + * @reason Optimise the collision for particles + * @author Spottedleaf + */ + @Redirect( + method = "move(DDD)V", + at = @At( + target = "Lnet/minecraft/world/entity/Entity;collideBoundingBox(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/AABB;Lnet/minecraft/world/level/Level;Ljava/util/List;)Lnet/minecraft/world/phys/Vec3;", + value = "INVOKE" + ) + ) + private Vec3 optimiseParticleCollisions(final Entity entity, final Vec3 movement, final AABB entityBoundingBox, final Level world, final List list) { + final AABB collisionBox; + + final boolean xEmpty = Math.abs(movement.x) < CollisionUtil.COLLISION_EPSILON; + final boolean yEmpty = Math.abs(movement.y) < CollisionUtil.COLLISION_EPSILON; + final boolean zEmpty = Math.abs(movement.z) < CollisionUtil.COLLISION_EPSILON; + + // try and get the smallest collision box possible by taking advantage of single-axis move + if (!xEmpty & (yEmpty | zEmpty)) { + if (movement.x < 0.0) { + collisionBox = CollisionUtil.cutLeft(entityBoundingBox, movement.x); + } else { + collisionBox = CollisionUtil.cutRight(entityBoundingBox, movement.x); + } + } else if (!yEmpty & (xEmpty | zEmpty)) { + if (movement.y < 0.0) { + collisionBox = CollisionUtil.cutDownwards(entityBoundingBox, movement.y); + } else { + collisionBox = CollisionUtil.cutUpwards(entityBoundingBox, movement.y); + } + } else if (!zEmpty & (xEmpty | yEmpty)) { + if (movement.z < 0.0) { + collisionBox = CollisionUtil.cutBackwards(entityBoundingBox, movement.z); + } else { + collisionBox = CollisionUtil.cutForwards(entityBoundingBox, movement.z); + } + } else { + collisionBox = entityBoundingBox.expandTowards(movement.x, movement.y, movement.z); + } + + final List boxes = new ArrayList<>(); + final List voxels = new ArrayList<>(); + final boolean collided = CollisionUtil.getCollisionsForBlocksOrWorldBorder( + world, entity, collisionBox, voxels, boxes, + 0, + null + ); + + if (!collided) { + // most of the time we fall here. + return movement; + } + + return CollisionUtil.performCollisions(movement, entityBoundingBox, voxels, boxes); + } + + /** + * @reason Optimise impl + * @author Spottedleaf + */ + @Overwrite + public int getLightColor(final float f) { + final int blockX = Mth.floor(this.x); + final int blockY = Mth.floor(this.y); + final int blockZ = Mth.floor(this.z); + + final BlockPos pos = new BlockPos(blockX, blockY, blockZ); + + final LevelChunk chunk = this.level.getChunkSource().getChunk(blockX >> 4, blockZ >> 4, ChunkStatus.FULL, false); + final BlockState blockState = chunk == null ? null : ((GetBlockChunk)chunk).getBlock(blockX, blockY, blockZ); + + if (blockState != null && !blockState.isAir()) { + if (blockState.emissiveRendering(this.level, pos)) { + return 15728880; + } + } + + final StarLightInterface lightEngine = ((StarLightLightingProvider)this.level.getLightEngine()).getLightEngine(); + + final int blockLight = Math.max(lightEngine.getBlockLightValue(pos, chunk), blockState == null ? 0 : blockState.getLightEmission()); + final int skyLight = lightEngine.getSkyLightValue(pos, chunk); + + return blockLight << 20 | skyLight << 4; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/PersistentEntitySectionManagerCallbackMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/PersistentEntitySectionManagerCallbackMixin.java new file mode 100644 index 0000000..479c2ed --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/PersistentEntitySectionManagerCallbackMixin.java @@ -0,0 +1,59 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.PersistentEntitySectionManager; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PersistentEntitySectionManager.Callback.class) +public abstract class PersistentEntitySectionManagerCallbackMixin { + + @Shadow + @Final + private T entity; + + @Shadow + private long currentSectionKey; + + /** + * @reason Hook into our entity slices + * @author Spottedleaf + */ + @Inject( + method = "onMove", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/EntitySection;remove(Lnet/minecraft/world/level/entity/EntityAccess;)Z" + ) + ) + private void changeSections(final CallbackInfo ci) { + final Entity entity = (Entity)this.entity; + + final long currentChunk = this.currentSectionKey; + + ((CollisionLevel)entity.level()).getCollisionLookup().moveEntity(entity, currentChunk); + } + + /** + * @reason Hook into our entity slices + * @author Spottedleaf + */ + @Inject( + method = "onRemove", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/EntitySection;remove(Lnet/minecraft/world/level/entity/EntityAccess;)Z" + ) + ) + private void onRemoved(final CallbackInfo ci) { + final Entity entity = (Entity)this.entity; + + ((CollisionLevel)entity.level()).getCollisionLookup().removeEntity(entity); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/PersistentEntitySectionManagerMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/PersistentEntitySectionManagerMixin.java new file mode 100644 index 0000000..e58a4a3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/PersistentEntitySectionManagerMixin.java @@ -0,0 +1,31 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.PersistentEntitySectionManager; +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.CallbackInfoReturnable; + +@Mixin(PersistentEntitySectionManager.class) +public abstract class PersistentEntitySectionManagerMixin { + + /** + * @reason Hook into our entity slices + * @author Spottedleaf + */ + @Inject( + method = "addEntity", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/EntitySection;add(Lnet/minecraft/world/level/entity/EntityAccess;)V" + ) + ) + private void addEntity(final T entityAccess, final boolean onDisk, final CallbackInfoReturnable cir) { + final Entity entity = (Entity)entityAccess; + + ((CollisionLevel)entity.level()).getCollisionLookup().addEntity(entity); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ServerEntityMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ServerEntityMixin.java new file mode 100644 index 0000000..d10f68e --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ServerEntityMixin.java @@ -0,0 +1,39 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.entity.CollisionEntity; +import net.minecraft.server.level.ServerEntity; +import net.minecraft.world.entity.Entity; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerEntity.class) +public abstract class ServerEntityMixin { + + @Shadow + @Final + private Entity entity; + + @Shadow + private int teleportDelay; + + /** + * @reason Position errors on hard colliding entities can cause collision issues on boats. To fix this, force any position + * updates to use teleport which uses full precision. + * @author Spottedleaf + */ + @Inject( + method = "sendChanges", + at = @At( + value = "HEAD" + ) + ) + private void forceHardCollideTeleport(final CallbackInfo ci) { + if (((CollisionEntity)this.entity).isHardColliding()) { + this.teleportDelay = 9999; + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ShapesMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ShapesMixin.java new file mode 100644 index 0000000..0580a0c --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ShapesMixin.java @@ -0,0 +1,383 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.CollisionUtil; +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import net.minecraft.Util; +import net.minecraft.core.Direction; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.ArrayVoxelShape; +import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape; +import net.minecraft.world.phys.shapes.BooleanOp; +import net.minecraft.world.phys.shapes.CubeVoxelShape; +import net.minecraft.world.phys.shapes.DiscreteCubeMerger; +import net.minecraft.world.phys.shapes.DiscreteVoxelShape; +import net.minecraft.world.phys.shapes.IndexMerger; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; +import java.util.Arrays; +import java.util.function.Supplier; + +@Mixin(Shapes.class) +public abstract class ShapesMixin { + + @Shadow + protected static int findBits(double d, double e) { + return 0; + } + + @Shadow + @Final + private static VoxelShape BLOCK; + + @Shadow + @Final + private static VoxelShape EMPTY; + + @Shadow + protected static IndexMerger createIndexMerger(int i, DoubleList doubleList, DoubleList doubleList2, boolean bl, boolean bl2) { + return null; + } + + @Unique + private static final boolean DEBUG_SHAPE_MERGING = false; + + /** + * Collisions are optimized for ArrayVoxelShape, so we should use that instead. + */ + @Redirect( + method = "", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/Util;make(Ljava/util/function/Supplier;)Ljava/lang/Object;" + ) + ) + private static Object forceArrayVoxelShape(final Supplier supplier) { + final DiscreteVoxelShape shape = new BitSetDiscreteVoxelShape(1, 1, 1); + shape.fill(0, 0, 0); + + return new ArrayVoxelShape( + shape, + CollisionUtil.ZERO_ONE, CollisionUtil.ZERO_ONE, CollisionUtil.ZERO_ONE + ); + } + + @Unique + private static final DoubleArrayList[] PARTS_BY_BITS = new DoubleArrayList[] { + DoubleArrayList.wrap(generateCubeParts(1 << 0)), + DoubleArrayList.wrap(generateCubeParts(1 << 1)), + DoubleArrayList.wrap(generateCubeParts(1 << 2)), + DoubleArrayList.wrap(generateCubeParts(1 << 3)) + }; + + @Unique + private static double[] generateCubeParts(final int parts) { + // note: parts is a power of two, so we do not need to worry about loss of precision here + // note: parts is from [2^0, 2^3] + final double inc = 1.0 / (double)parts; + + final double[] ret = new double[parts + 1]; + double val = 0.0; + for (int i = 0; i <= parts; ++i) { + ret[i] = val; + val += inc; + } + + return ret; + } + + /** + * @reason Avoid creating CubeVoxelShape instances + * @author Spottedleaf + */ + @Overwrite + public static VoxelShape create(final double minX, final double minY, final double minZ, final double maxX, final double maxY, final double maxZ) { + if (!(maxX - minX < 1.0E-7) && !(maxY - minY < 1.0E-7) && !(maxZ - minZ < 1.0E-7)) { + final int bitsX = findBits(minX, maxX); + final int bitsY = findBits(minY, maxY); + final int bitsZ = findBits(minZ, maxZ); + if (bitsX >= 0 && bitsY >= 0 && bitsZ >= 0) { + if (bitsX == 0 && bitsY == 0 && bitsZ == 0) { + return BLOCK; + } else { + final int sizeX = 1 << bitsX; + final int sizeY = 1 << bitsY; + final int sizeZ = 1 << bitsZ; + final BitSetDiscreteVoxelShape shape = BitSetDiscreteVoxelShape.withFilledBounds( + sizeX, sizeY, sizeZ, + (int)Math.round(minX * (double)sizeX), (int)Math.round(minY * (double)sizeY), (int)Math.round(minZ * (double)sizeZ), + (int)Math.round(maxX * (double)sizeX), (int)Math.round(maxY * (double)sizeY), (int)Math.round(maxZ * (double)sizeZ) + ); + return new ArrayVoxelShape( + shape, + PARTS_BY_BITS[bitsX], + PARTS_BY_BITS[bitsY], + PARTS_BY_BITS[bitsZ] + ); + } + } else { + return new ArrayVoxelShape( + BLOCK.shape, + minX == 0.0 && maxX == 1.0 ? CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minX, maxX }), + minY == 0.0 && maxY == 1.0 ? CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minY, maxY }), + minZ == 0.0 && maxZ == 1.0 ? CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minZ, maxZ }) + ); + } + } else { + return EMPTY; + } + } + + /** + * @reason Stop using streams + * @author Spottedleaf + */ + @Overwrite + public static VoxelShape or(final VoxelShape shape, final VoxelShape... others) { + int size = others.length; + if (size == 0) { + return shape; + } + + // reduce complexity of joins by splitting the merges + + // add extra slot for first shape + ++size; + final VoxelShape[] tmp = Arrays.copyOf(others, size); + // insert first shape + tmp[size - 1] = shape; + + while (size > 1) { + int newSize = 0; + for (int i = 0; i < size; i += 2) { + final int next = i + 1; + if (next >= size) { + // nothing to merge with, so leave it for next iteration + tmp[newSize++] = tmp[i]; + break; + } else { + // merge with adjacent + final VoxelShape first = tmp[i]; + final VoxelShape second = tmp[next]; + + tmp[newSize++] = Shapes.join(first, second, BooleanOp.OR); + } + } + size = newSize; + } + + return tmp[0]; + } + + @Unique + private static VoxelShape joinUnoptimizedVanilla(final VoxelShape voxelShape, final VoxelShape voxelShape2, final BooleanOp booleanOp) { + if (booleanOp.apply(false, false)) { + throw (IllegalArgumentException) Util.pauseInIde(new IllegalArgumentException()); + } else if (voxelShape == voxelShape2) { + return booleanOp.apply(true, true) ? voxelShape : EMPTY; + } else { + boolean bl = booleanOp.apply(true, false); + boolean bl2 = booleanOp.apply(false, true); + if (voxelShape.isEmpty()) { + return bl2 ? voxelShape2 : EMPTY; + } else if (voxelShape2.isEmpty()) { + return bl ? voxelShape : EMPTY; + } else { + IndexMerger indexMerger = createIndexMerger(1, voxelShape.getCoords(Direction.Axis.X), voxelShape2.getCoords(Direction.Axis.X), bl, bl2); + IndexMerger indexMerger2 = createIndexMerger(indexMerger.size() - 1, voxelShape.getCoords(Direction.Axis.Y), voxelShape2.getCoords(Direction.Axis.Y), bl, bl2); + IndexMerger indexMerger3 = createIndexMerger((indexMerger.size() - 1) * (indexMerger2.size() - 1), voxelShape.getCoords(Direction.Axis.Z), voxelShape2.getCoords(Direction.Axis.Z), bl, bl2); + BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.join(voxelShape.shape, voxelShape2.shape, indexMerger, indexMerger2, indexMerger3, booleanOp); + return (VoxelShape) (indexMerger instanceof DiscreteCubeMerger && indexMerger2 instanceof DiscreteCubeMerger && indexMerger3 instanceof DiscreteCubeMerger ? new CubeVoxelShape(bitSetDiscreteVoxelShape) : new ArrayVoxelShape(bitSetDiscreteVoxelShape, indexMerger.getList(), indexMerger2.getList(), indexMerger3.getList())); + } + } + } + + /** + * @reason Route to faster logic + * @author Spottedleaf + */ + @Overwrite + public static VoxelShape join(final VoxelShape first, final VoxelShape second, final BooleanOp mergeFunction) { + final VoxelShape ret = CollisionUtil.joinOptimized(first, second, mergeFunction); + if (DEBUG_SHAPE_MERGING) { + final VoxelShape vanilla = joinUnoptimizedVanilla(first, second, mergeFunction); + if (!CollisionUtil.equals(ret, vanilla.optimize())) { + CollisionUtil.joinUnoptimized(first, second, mergeFunction); + joinUnoptimizedVanilla(first, second, mergeFunction); + throw new IllegalStateException("TRAP"); + } + } + return ret; + } + + /** + * @reason Route to faster logic + * @author Spottedleaf + */ + @Overwrite + public static VoxelShape joinUnoptimized(final VoxelShape first, final VoxelShape second, final BooleanOp mergeFunction) { + final VoxelShape ret = CollisionUtil.joinUnoptimized(first, second, mergeFunction); + if (DEBUG_SHAPE_MERGING) { + final VoxelShape vanilla = joinUnoptimizedVanilla(first, second, mergeFunction); + if (!CollisionUtil.equals(ret, vanilla)) { + CollisionUtil.joinUnoptimized(first, second, mergeFunction); + joinUnoptimizedVanilla(first, second, mergeFunction); + throw new IllegalStateException("TRAP"); + } + } + return ret; + } + + /** + * @reason Route to faster logic + * @author Spottedleaf + */ + @Overwrite + public static boolean joinIsNotEmpty(final VoxelShape first, final VoxelShape second, final BooleanOp mergeFunction) { + final boolean ret = CollisionUtil.isJoinNonEmpty(first, second, mergeFunction); + if (DEBUG_SHAPE_MERGING) { + if (ret != !joinUnoptimizedVanilla(first, second, mergeFunction).isEmpty()) { + CollisionUtil.isJoinNonEmpty(first, second, mergeFunction); + joinUnoptimizedVanilla(first, second, mergeFunction).isEmpty(); + throw new IllegalStateException("TRAP"); + } + } + return ret; + } + + /** + * @reason Route to use cache + * @author Spottedleaf + */ + @Overwrite + public static VoxelShape getFaceShape(final VoxelShape shape, final Direction direction) { + return ((CollisionVoxelShape)shape).getFaceShapeClamped(direction); + } + + @Unique + private static boolean mergedMayOccludeBlock(final VoxelShape shape1, final VoxelShape shape2) { + // if the combined bounds of the two shapes cannot occlude, then neither can the merged + final AABB bounds1 = shape1.bounds(); + final AABB bounds2 = shape2.bounds(); + + final double minX = Math.min(bounds1.minX, bounds2.minX); + final double maxX = Math.max(bounds1.maxX, bounds2.maxX); + final double minY = Math.min(bounds1.minY, bounds2.minY); + final double maxY = Math.max(bounds1.maxY, bounds2.maxY); + final double minZ = Math.min(bounds1.minZ, bounds2.minZ); + final double maxZ = Math.max(bounds1.maxZ, bounds2.maxZ); + + return (minX <= CollisionUtil.COLLISION_EPSILON && maxX >= (1 - CollisionUtil.COLLISION_EPSILON)) && + (minY <= CollisionUtil.COLLISION_EPSILON && maxY >= (1 - CollisionUtil.COLLISION_EPSILON)) && + (minZ <= CollisionUtil.COLLISION_EPSILON && maxZ >= (1 - CollisionUtil.COLLISION_EPSILON)); + } + + /** + * @reason Route to faster logic + * @author Spottedleaf + */ + @Overwrite + public static boolean mergedFaceOccludes(final VoxelShape first, final VoxelShape second, final Direction direction) { + // see if any of the shapes on their own occludes, only if cached + if (((CollisionVoxelShape)first).occludesFullBlockIfCached() || ((CollisionVoxelShape)second).occludesFullBlockIfCached()) { + return true; + } + + if (first.isEmpty() & second.isEmpty()) { + return false; + } + + // we optimise getOpposite, so we can use it + // secondly, use our cache to retrieve sliced shape + final VoxelShape newFirst = ((CollisionVoxelShape)first).getFaceShapeClamped(direction); + final VoxelShape newSecond = ((CollisionVoxelShape)second).getFaceShapeClamped(direction.getOpposite()); + + // see if any of the shapes on their own occludes, only if cached + if (((CollisionVoxelShape)newFirst).occludesFullBlockIfCached() || ((CollisionVoxelShape)newSecond).occludesFullBlockIfCached()) { + return true; + } + + final boolean firstEmpty = newFirst.isEmpty(); + final boolean secondEmpty = newSecond.isEmpty(); + + if (firstEmpty & secondEmpty) { + return false; + } + + if (firstEmpty | secondEmpty) { + return secondEmpty ? ((CollisionVoxelShape)newFirst).occludesFullBlock() : ((CollisionVoxelShape)newSecond).occludesFullBlock(); + } + + if (newFirst == newSecond) { + return ((CollisionVoxelShape)newFirst).occludesFullBlock(); + } + + return mergedMayOccludeBlock(newFirst, newSecond) && ((CollisionVoxelShape)((CollisionVoxelShape)newFirst).orUnoptimized(newSecond)).occludesFullBlock(); + } + + /** + * @reason Route to faster logic + * @author Spottedleaf + */ + @Overwrite + public static boolean blockOccudes(final VoxelShape first, final VoxelShape second, final Direction direction) { + final boolean firstBlock = first == BLOCK; + final boolean secondBlock = second == BLOCK; + + if (firstBlock & secondBlock) { + return true; + } + + if (first.isEmpty() | second.isEmpty()) { + return false; + } + + // we optimise getOpposite, so we can use it + // secondly, use our cache to retrieve sliced shape + final VoxelShape newFirst = ((CollisionVoxelShape)first).getFaceShapeClamped(direction); + if (newFirst.isEmpty()) { + return false; + } + final VoxelShape newSecond = ((CollisionVoxelShape)second).getFaceShapeClamped(direction.getOpposite()); + if (newSecond.isEmpty()) { + return false; + } + + return !joinIsNotEmpty(newFirst, newSecond, BooleanOp.ONLY_FIRST); + } + + /** + * @reason Route to faster logic + * @author Spottedleaf + */ + @Overwrite + public static boolean faceShapeOccludes(final VoxelShape shape1, final VoxelShape shape2) { + if (((CollisionVoxelShape)shape1).occludesFullBlockIfCached() || ((CollisionVoxelShape)shape2).occludesFullBlockIfCached()) { + return true; + } + + final boolean s1Empty = shape1.isEmpty(); + final boolean s2Empty = shape2.isEmpty(); + if (s1Empty & s2Empty) { + return false; + } + + if (s1Empty | s2Empty) { + return s2Empty ? ((CollisionVoxelShape)shape1).occludesFullBlock() : ((CollisionVoxelShape)shape2).occludesFullBlock(); + } + + if (shape1 == shape2) { + return ((CollisionVoxelShape)shape1).occludesFullBlock(); + } + + return mergedMayOccludeBlock(shape1, shape2) && ((CollisionVoxelShape)((CollisionVoxelShape)shape1).orUnoptimized(shape2)).occludesFullBlock(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/SliceShapeMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/SliceShapeMixin.java new file mode 100644 index 0000000..d34d5f4 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/SliceShapeMixin.java @@ -0,0 +1,28 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; +import net.minecraft.core.Direction; +import net.minecraft.world.phys.shapes.SliceShape; +import net.minecraft.world.phys.shapes.VoxelShape; +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; + +@Mixin(SliceShape.class) +public abstract class SliceShapeMixin { + + /** + * @reason Hook into the root constructor to pass along init data to superclass. + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void initState(final VoxelShape parent, final Direction.Axis forAxis, final int forIndex, final CallbackInfo ci) { + ((CollisionVoxelShape)this).initCache(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/TransientEntitySectionManagerCallbackMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/TransientEntitySectionManagerCallbackMixin.java new file mode 100644 index 0000000..1810df7 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/TransientEntitySectionManagerCallbackMixin.java @@ -0,0 +1,59 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.TransientEntitySectionManager; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(TransientEntitySectionManager.Callback.class) +public abstract class TransientEntitySectionManagerCallbackMixin { + + @Shadow + @Final + private T entity; + + @Shadow + private long currentSectionKey; + + /** + * @reason Hook into our entity slices + * @author Spottedleaf + */ + @Inject( + method = "onMove", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/EntitySection;remove(Lnet/minecraft/world/level/entity/EntityAccess;)Z" + ) + ) + private void changeSections(final CallbackInfo ci) { + final Entity entity = (Entity)this.entity; + + final long currentChunk = this.currentSectionKey; + + ((CollisionLevel)entity.level()).getCollisionLookup().moveEntity(entity, currentChunk); + } + + /** + * @reason Hook into our entity slices + * @author Spottedleaf + */ + @Inject( + method = "onRemove", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/EntitySection;remove(Lnet/minecraft/world/level/entity/EntityAccess;)Z" + ) + ) + private void onRemoved(final CallbackInfo ci) { + final Entity entity = (Entity)this.entity; + + ((CollisionLevel)entity.level()).getCollisionLookup().removeEntity(entity); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/TransientEntitySectionManagerMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/TransientEntitySectionManagerMixin.java new file mode 100644 index 0000000..3c656cc --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/TransientEntitySectionManagerMixin.java @@ -0,0 +1,31 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.TransientEntitySectionManager; +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; + +@Mixin(TransientEntitySectionManager.class) +public abstract class TransientEntitySectionManagerMixin { + + /** + * @reason Hook into our entity slices + * @author Spottedleaf + */ + @Inject( + method = "addEntity", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/EntitySection;add(Lnet/minecraft/world/level/entity/EntityAccess;)V" + ) + ) + private void addEntity(final T entityAccess, final CallbackInfo ci) { + final Entity entity = (Entity)entityAccess; + + ((CollisionLevel)entity.level()).getCollisionLookup().addEntity(entity); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/VoxelShapeMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/VoxelShapeMixin.java new file mode 100644 index 0000000..b406bd6 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/VoxelShapeMixin.java @@ -0,0 +1,745 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.patches.collisions.CollisionUtil; +import ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData; +import ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs; +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape; +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; +import ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache; +import com.google.common.math.DoubleMath; +import it.unimi.dsi.fastutil.HashCommon; +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.ArrayVoxelShape; +import net.minecraft.world.phys.shapes.BooleanOp; +import net.minecraft.world.phys.shapes.DiscreteVoxelShape; +import net.minecraft.world.phys.shapes.OffsetDoubleList; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.SliceShape; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Mixin(VoxelShape.class) +public abstract class VoxelShapeMixin implements CollisionVoxelShape { + + @Shadow + public abstract DoubleList getCoords(final Direction.Axis axis); + + @Shadow + @Final + public DiscreteVoxelShape shape; + + @Shadow + public abstract void forAllBoxes(final Shapes.DoubleLineConsumer doubleLineConsumer); + + @Unique + private double offsetX; + @Unique + private double offsetY; + @Unique + private double offsetZ; + @Unique + private AABB singleAABBRepresentation; + @Unique + private double[] rootCoordinatesX; + @Unique + private double[] rootCoordinatesY; + @Unique + private double[] rootCoordinatesZ; + + @Unique + private CachedShapeData cachedShapeData; + @Unique + private boolean isEmpty; + + @Unique + private CachedToAABBs cachedToAABBs; + @Unique + private AABB cachedBounds; + + @Unique + private Boolean isFullBlock; + + @Unique + private Boolean occludesFullBlock; + + // must be power of two + @Unique + private static final int MERGED_CACHE_SIZE = 16; + + @Unique + private MergedORCache[] mergedORCache; + + @Override + public final double offsetX() { + return this.offsetX; + } + + @Override + public final double offsetY() { + return this.offsetY; + } + + @Override + public final double offsetZ() { + return this.offsetZ; + } + + @Override + public final AABB getSingleAABBRepresentation() { + return this.singleAABBRepresentation; + } + + @Override + public final double[] rootCoordinatesX() { + return this.rootCoordinatesX; + } + + @Override + public final double[] rootCoordinatesY() { + return this.rootCoordinatesY; + } + + @Override + public final double[] rootCoordinatesZ() { + return this.rootCoordinatesZ; + } + + private static double[] extractRawArray(final DoubleList list) { + if (list instanceof DoubleArrayList rawList) { + final double[] raw = rawList.elements(); + final int expected = rawList.size(); + if (raw.length == expected) { + return raw; + } else { + return Arrays.copyOf(raw, expected); + } + } else { + return list.toDoubleArray(); + } + } + + @Override + public final void initCache() { + this.cachedShapeData = ((CollisionDiscreteVoxelShape)this.shape).getOrCreateCachedShapeData(); + this.isEmpty = this.cachedShapeData.isEmpty(); + + final DoubleList xList = this.getCoords(Direction.Axis.X); + final DoubleList yList = this.getCoords(Direction.Axis.Y); + final DoubleList zList = this.getCoords(Direction.Axis.Z); + + if (xList instanceof OffsetDoubleList offsetDoubleList) { + this.offsetX = offsetDoubleList.offset; + this.rootCoordinatesX = extractRawArray(offsetDoubleList.delegate); + } else { + this.rootCoordinatesX = extractRawArray(xList); + } + + if (yList instanceof OffsetDoubleList offsetDoubleList) { + this.offsetY = offsetDoubleList.offset; + this.rootCoordinatesY = extractRawArray(offsetDoubleList.delegate); + } else { + this.rootCoordinatesY = extractRawArray(yList); + } + + if (zList instanceof OffsetDoubleList offsetDoubleList) { + this.offsetZ = offsetDoubleList.offset; + this.rootCoordinatesZ = extractRawArray(offsetDoubleList.delegate); + } else { + this.rootCoordinatesZ = extractRawArray(zList); + } + + if (this.cachedShapeData.hasSingleAABB()) { + this.singleAABBRepresentation = new AABB( + this.rootCoordinatesX[0] + this.offsetX, this.rootCoordinatesY[0] + this.offsetY, this.rootCoordinatesZ[0] + this.offsetZ, + this.rootCoordinatesX[1] + this.offsetX, this.rootCoordinatesY[1] + this.offsetY, this.rootCoordinatesZ[1] + this.offsetZ + ); + this.cachedBounds = this.singleAABBRepresentation; + } + } + + @Override + public final CachedShapeData getCachedVoxelData() { + return this.cachedShapeData; + } + + @Unique + private VoxelShape[] faceShapeClampedCache; + + @Override + public final VoxelShape getFaceShapeClamped(final Direction direction) { + if (this.isEmpty) { + return (VoxelShape)(Object)this; + } + if ((VoxelShape)(Object)this == Shapes.block()) { + return (VoxelShape)(Object)this; + } + + VoxelShape[] cache = this.faceShapeClampedCache; + if (cache != null) { + final VoxelShape ret = cache[direction.ordinal()]; + if (ret != null) { + return ret; + } + } + + + if (cache == null) { + this.faceShapeClampedCache = cache = new VoxelShape[6]; + } + + final Direction.Axis axis = direction.getAxis(); + + final VoxelShape ret; + + if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) { + if (DoubleMath.fuzzyEquals(this.max(axis), 1.0, CollisionUtil.COLLISION_EPSILON)) { + ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, this.shape.getSize(axis) - 1)); + } else { + ret = Shapes.empty(); + } + } else { + if (DoubleMath.fuzzyEquals(this.min(axis), 0.0, CollisionUtil.COLLISION_EPSILON)) { + ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, 0)); + } else { + ret = Shapes.empty(); + } + } + + cache[direction.ordinal()] = ret; + + return ret; + } + + @Unique + private static VoxelShape tryForceBlock(final VoxelShape other) { + if (other == Shapes.block()) { + return other; + } + + final AABB otherAABB = ((CollisionVoxelShape)other).getSingleAABBRepresentation(); + if (otherAABB == null) { + return other; + } + + if (((CollisionVoxelShape)Shapes.block()).getSingleAABBRepresentation().equals(otherAABB)) { + return Shapes.block(); + } + + return other; + } + + @Unique + private boolean computeOccludesFullBlock() { + if (this.isEmpty) { + this.occludesFullBlock = Boolean.FALSE; + return false; + } + + if (this.isFullBlock()) { + this.occludesFullBlock = Boolean.TRUE; + return true; + } + + final AABB singleAABB = this.singleAABBRepresentation; + if (singleAABB != null) { + // check if the bounding box encloses the full cube + final boolean ret = (singleAABB.minY <= CollisionUtil.COLLISION_EPSILON && singleAABB.maxY >= (1 - CollisionUtil.COLLISION_EPSILON)) && + (singleAABB.minX <= CollisionUtil.COLLISION_EPSILON && singleAABB.maxX >= (1 - CollisionUtil.COLLISION_EPSILON)) && + (singleAABB.minZ <= CollisionUtil.COLLISION_EPSILON && singleAABB.maxZ >= (1 - CollisionUtil.COLLISION_EPSILON)); + this.occludesFullBlock = Boolean.valueOf(ret); + return ret; + } + + final boolean ret = !Shapes.joinIsNotEmpty(Shapes.block(), ((VoxelShape)(Object)this), BooleanOp.ONLY_FIRST); + this.occludesFullBlock = Boolean.valueOf(ret); + return ret; + } + + @Override + public final boolean occludesFullBlock() { + final Boolean ret = this.occludesFullBlock; + if (ret != null) { + return ret.booleanValue(); + } + + return this.computeOccludesFullBlock(); + } + + @Override + public final boolean occludesFullBlockIfCached() { + final Boolean ret = this.occludesFullBlock; + return ret != null ? ret.booleanValue() : false; + } + + @Unique + private static int hash(final VoxelShape key) { + return HashCommon.mix(System.identityHashCode(key)); + } + + @Override + public final VoxelShape orUnoptimized(final VoxelShape other) { + // don't cache simple cases + if (((VoxelShape)(Object)this) == other) { + return other; + } + + if (this.isEmpty) { + return other; + } + + if (other.isEmpty()) { + return (VoxelShape)(Object)this; + } + + // try this cache first + final int thisCacheKey = hash(other) & (MERGED_CACHE_SIZE - 1); + final MergedORCache cached = this.mergedORCache == null ? null : this.mergedORCache[thisCacheKey]; + if (cached != null && cached.key() == other) { + return cached.result(); + } + + // try other cache + final int otherCacheKey = hash((VoxelShape)(Object)this) & (MERGED_CACHE_SIZE - 1); + final MergedORCache otherCache = ((VoxelShapeMixin)(Object)other).mergedORCache == null ? null : ((VoxelShapeMixin)(Object)other).mergedORCache[otherCacheKey]; + if (otherCache != null && otherCache.key() == (VoxelShape)(Object)this) { + return otherCache.result(); + } + + // note: unsure if joinUnoptimized(1, 2, OR) == joinUnoptimized(2, 1, OR) for all cases + final VoxelShape result = Shapes.joinUnoptimized((VoxelShape)(Object)this, other, BooleanOp.OR); + + if (cached != null && otherCache == null) { + // try to use second cache instead of replacing an entry in this cache + if (((VoxelShapeMixin)(Object)other).mergedORCache == null) { + ((VoxelShapeMixin)(Object)other).mergedORCache = new MergedORCache[MERGED_CACHE_SIZE]; + } + ((VoxelShapeMixin)(Object)other).mergedORCache[otherCacheKey] = new MergedORCache((VoxelShape)(Object)this, result); + } else { + // line is not occupied or other cache line is full + // always bias to replace this cache, as this cache is the first we check + if (this.mergedORCache == null) { + this.mergedORCache = new MergedORCache[MERGED_CACHE_SIZE]; + } + this.mergedORCache[thisCacheKey] = new MergedORCache(other, result); + } + + return result; + } + + // mixin hooks + + /** + * @author Spottedleaf + * @reason Use cached value instead + */ + @Overwrite + public boolean isEmpty() { + return this.isEmpty; + } + + /** + * @author Spottedleaf + * @reason Route to optimized collision method + */ + @Overwrite + public double collide(final Direction.Axis axis, final AABB source, final double source_move) { + if (this.isEmpty) { + return source_move; + } + if (Math.abs(source_move) < CollisionUtil.COLLISION_EPSILON) { + return 0.0; + } + switch (axis) { + case X: { + return CollisionUtil.collideX((VoxelShape)(Object)this, source, source_move); + } + case Y: { + return CollisionUtil.collideY((VoxelShape)(Object)this, source, source_move); + } + case Z: { + return CollisionUtil.collideZ((VoxelShape)(Object)this, source, source_move); + } + default: { + throw new RuntimeException("Unknown axis: " + axis); + } + } + } + + @Unique + private static DoubleList offsetList(final DoubleList src, final double by) { + if (src instanceof OffsetDoubleList offsetDoubleList) { + return new OffsetDoubleList(offsetDoubleList.delegate, by + offsetDoubleList.offset); + } + return new OffsetDoubleList(src, by); + } + + /** + * @author Spottedleaf + * @reason Do not nest offset double lists + */ + @Overwrite + public VoxelShape move(final double x, final double y, final double z) { + if (this.isEmpty) { + return Shapes.empty(); + } + + final ArrayVoxelShape ret = new ArrayVoxelShape( + this.shape, + offsetList(this.getCoords(Direction.Axis.X), x), + offsetList(this.getCoords(Direction.Axis.Y), y), + offsetList(this.getCoords(Direction.Axis.Z), z) + ); + + final CachedToAABBs cachedToAABBs = this.cachedToAABBs; + if (cachedToAABBs != null) { + ((VoxelShapeMixin)(Object)ret).cachedToAABBs = CachedToAABBs.offset(cachedToAABBs, x, y, z); + } + + return ret; + } + + @Unique + private List toAabbsUncached() { + final List ret = new ArrayList<>(); + if (this.singleAABBRepresentation != null) { + ret.add(this.singleAABBRepresentation); + } else { + this.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { + ret.add(new AABB(minX, minY, minZ, maxX, maxY, maxZ)); + }); + } + + // cache result + this.cachedToAABBs = new CachedToAABBs(ret, false, 0.0, 0.0, 0.0); + + return ret; + } + + /** + * @author Spottedleaf + * @reason Cache toAABBs result + */ + @Overwrite + public List toAabbs() { + CachedToAABBs cachedToAABBs = this.cachedToAABBs; + if (cachedToAABBs != null) { + if (!cachedToAABBs.isOffset()) { + return cachedToAABBs.aabbs(); + } + + // all we need to do is offset the cache + cachedToAABBs = cachedToAABBs.removeOffset(); + // update cache + this.cachedToAABBs = cachedToAABBs; + + return cachedToAABBs.aabbs(); + } + + // make new cache + return this.toAabbsUncached(); + } + + @Unique + private boolean computeFullBlock() { + final Boolean ret; + if (this.isEmpty) { + ret = Boolean.FALSE; + } else if ((VoxelShape)(Object)this == Shapes.block()) { + ret = Boolean.TRUE; + } else { + final AABB singleAABB = this.singleAABBRepresentation; + if (singleAABB == null) { + // note: Shapes.join(BLOCK, this, NOT_SAME) cannot be empty when voxelSize > 2 + ret = Boolean.FALSE; + } else { + ret = Boolean.valueOf( + Math.abs(singleAABB.minX) <= CollisionUtil.COLLISION_EPSILON && + Math.abs(singleAABB.minY) <= CollisionUtil.COLLISION_EPSILON && + Math.abs(singleAABB.minZ) <= CollisionUtil.COLLISION_EPSILON && + + Math.abs(1.0 - singleAABB.maxX) <= CollisionUtil.COLLISION_EPSILON && + Math.abs(1.0 - singleAABB.maxY) <= CollisionUtil.COLLISION_EPSILON && + Math.abs(1.0 - singleAABB.maxZ) <= CollisionUtil.COLLISION_EPSILON + ); + } + } + + this.isFullBlock = ret; + + return ret.booleanValue(); + } + + @Override + public boolean isFullBlock() { + final Boolean ret = this.isFullBlock; + + if (ret != null) { + return ret.booleanValue(); + } + + return this.computeFullBlock(); + } + + /** + * Copy of AABB#clip but for one AABB + */ + @Unique + private static BlockHitResult clip(final AABB aabb, final Vec3 from, final Vec3 to, final BlockPos offset) { + final double[] minDistanceArr = new double[] { 1.0 }; + final double diffX = to.x - from.x; + final double diffY = to.y - from.y; + final double diffZ = to.z - from.z; + + final Direction direction = AABB.getDirection(aabb.move(offset), from, minDistanceArr, null, diffX, diffY, diffZ); + + if (direction == null) { + return null; + } + + final double minDistance = minDistanceArr[0]; + return new BlockHitResult(from.add(minDistance * diffX, minDistance * diffY, minDistance * diffZ), direction, offset, false); + } + + /** + * @reason Use single cached AABB for clipping if possible + * @author Spottedleaf + */ + @Overwrite + public BlockHitResult clip(final Vec3 from, final Vec3 to, final BlockPos offset) { + if (this.isEmpty) { + return null; + } + + final Vec3 directionOpposite = to.subtract(from); + if (directionOpposite.lengthSqr() < CollisionUtil.COLLISION_EPSILON) { + return null; + } + + final Vec3 fromBehind = from.add(directionOpposite.scale(0.001)); + final double fromBehindOffsetX = fromBehind.x - (double)offset.getX(); + final double fromBehindOffsetY = fromBehind.y - (double)offset.getY(); + final double fromBehindOffsetZ = fromBehind.z - (double)offset.getZ(); + + final AABB singleAABB = this.singleAABBRepresentation; + if (singleAABB != null) { + if (singleAABB.contains(fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) { + return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true); + } + return clip(singleAABB, from, to, offset); + } + + if (CollisionUtil.strictlyContains((VoxelShape)(Object)this, fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) { + return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true); + } + + return AABB.clip(((VoxelShape)(Object)this).toAabbs(), from, to, offset); + } + + @Unique + private boolean multiAABBClips(final double fromX, final double fromY, final double fromZ, + final double directionInvX, final double directionInvY, final double directionInvZ, + final double tMax) { + final List aabbs = this.toAabbs(); + for (int i = 0, len = aabbs.size(); i < len; ++i) { + final AABB box = aabbs.get(i); + if (CollisionUtil.clips(box, fromX, fromY, fromZ, directionInvX, directionInvY, directionInvZ, tMax)) { + return true; + } + } + + return false; + } + + @Override + public boolean doesClip(final double fromX, final double fromY, final double fromZ, + final double directionInvX, final double directionInvY, final double directionInvZ, + final double tMax) { + if (this.isEmpty) { + return false; + } + + final AABB singleAABB = this.singleAABBRepresentation; + if (singleAABB != null) { + return CollisionUtil.clips(singleAABB, fromX, fromY, fromZ, directionInvX, directionInvY, directionInvZ, tMax); + } + + return this.multiAABBClips(fromX, fromY, fromZ, directionInvX, directionInvY, directionInvZ, tMax); + } + + /** + * @reason Cache bounds + * @author Spottedleaf + */ + @Overwrite + public AABB bounds() { + if (this.isEmpty) { + throw Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape.")); + } + AABB cached = this.cachedBounds; + if (cached != null) { + return cached; + } + + final CachedShapeData shapeData = this.cachedShapeData; + + final double[] coordsX = this.rootCoordinatesX; + final double[] coordsY = this.rootCoordinatesY; + final double[] coordsZ = this.rootCoordinatesZ; + + final double offX = this.offsetX; + final double offY = this.offsetY; + final double offZ = this.offsetZ; + + // note: if not empty, then there is one full AABB so no bounds checks are needed on the minFull/maxFull indices + cached = new AABB( + coordsX[shapeData.minFullX()] + offX, + coordsY[shapeData.minFullY()] + offY, + coordsZ[shapeData.minFullZ()] + offZ, + + coordsX[shapeData.maxFullX()] + offX, + coordsY[shapeData.maxFullY()] + offY, + coordsZ[shapeData.maxFullZ()] + offZ + ); + + this.cachedBounds = cached; + return cached; + } + + /** + * @reason Reduce indirection from axis + * @author Spottedleaf + */ + @Overwrite + public double min(final Direction.Axis axis) { + final CachedShapeData shapeData = this.cachedShapeData; + switch (axis) { + case X: { + final int idx = shapeData.minFullX(); + return idx >= shapeData.sizeX() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX); + } + case Y: { + final int idx = shapeData.minFullY(); + return idx >= shapeData.sizeY() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY); + } + case Z: { + final int idx = shapeData.minFullZ(); + return idx >= shapeData.sizeZ() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ); + } + default: { + // should never get here + return Double.POSITIVE_INFINITY; + } + } + } + + /** + * @reason Reduce indirection from axis + * @author Spottedleaf + */ + @Overwrite + public double max(final Direction.Axis axis) { + final CachedShapeData shapeData = this.cachedShapeData; + switch (axis) { + case X: { + final int idx = shapeData.maxFullX(); + return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX); + } + case Y: { + final int idx = shapeData.maxFullY(); + return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY); + } + case Z: { + final int idx = shapeData.maxFullZ(); + return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ); + } + default: { + // should never get here + return Double.NEGATIVE_INFINITY; + } + } + } + + + /** + * @reason Optimise merge strategy to increase the number of simple joins, and additionally forward the toAabbs cache + * to result + * @author Spottedleaf + */ + @Overwrite + public VoxelShape optimize() { + if (this.isEmpty) { + return Shapes.empty(); + } + + if (this.singleAABBRepresentation != null) { + return this.isFullBlock() ? Shapes.block() : (VoxelShape)(Object)this; + } + + final List aabbs = this.toAabbs(); + + if (aabbs.size() == 1) { + final AABB singleAABB = aabbs.get(0); + final VoxelShape ret = Shapes.create(singleAABB); + + // forward AABB cache + if (((VoxelShapeMixin)(Object)ret).cachedToAABBs == null) { + ((VoxelShapeMixin)(Object)ret).cachedToAABBs = this.cachedToAABBs; + } + + return ret; + } else { + // reduce complexity of joins by splitting the merges (old complexity: n^2, new: nlogn) + + // set up flat array so that this merge is done in-place + final VoxelShape[] tmp = new VoxelShape[aabbs.size()]; + + // initialise as unmerged + for (int i = 0, len = aabbs.size(); i < len; ++i) { + tmp[i] = Shapes.create(aabbs.get(i)); + } + + int size = aabbs.size(); + while (size > 1) { + int newSize = 0; + for (int i = 0; i < size; i += 2) { + final int next = i + 1; + if (next >= size) { + // nothing to merge with, so leave it for next iteration + tmp[newSize++] = tmp[i]; + break; + } else { + // merge with adjacent + final VoxelShape first = tmp[i]; + final VoxelShape second = tmp[next]; + + tmp[newSize++] = Shapes.joinUnoptimized(first, second, BooleanOp.OR); + } + } + size = newSize; + } + + final VoxelShape ret = tmp[0]; + + // forward AABB cache + if (((VoxelShapeMixin)(Object)ret).cachedToAABBs == null) { + ((VoxelShapeMixin)(Object)ret).cachedToAABBs = this.cachedToAABBs; + } + + return ret; + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/datawatcher/SynchedEntityDataMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/datawatcher/SynchedEntityDataMixin.java new file mode 100644 index 0000000..549686a --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/datawatcher/SynchedEntityDataMixin.java @@ -0,0 +1,157 @@ +package ca.spottedleaf.moonrise.mixin.datawatcher; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.world.entity.Entity; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Mixin(SynchedEntityData.class) +public abstract class SynchedEntityDataMixin { + @Shadow + @Final + private Int2ObjectMap> itemsById; + + @Shadow + private boolean isDirty; + + @Shadow + @Final + private Entity entity; + + @Shadow + protected abstract void assignValue(SynchedEntityData.DataItem dataItem, SynchedEntityData.DataValue dataValue); + + + + + @Unique + private static final SynchedEntityData.DataItem[] EMPTY = new SynchedEntityData.DataItem[0]; + + @Unique + private SynchedEntityData.DataItem[] itemsByArray = EMPTY; + + /** + * @reason Remove unnecessary locking, and use the array lookup + * @author Spottedleaf + */ + @Overwrite + private void createDataItem(final EntityDataAccessor entityDataAccessor, final T dfl) { + final int id = entityDataAccessor.getId(); + final SynchedEntityData.DataItem dataItem = new SynchedEntityData.DataItem<>(entityDataAccessor, dfl); + + this.itemsById.put(id, dataItem); + if (id >= this.itemsByArray.length) { + this.itemsByArray = Arrays.copyOf(this.itemsByArray, Math.max(4, id << 1)); + } + + this.itemsByArray[id] = dataItem; + } + + /** + * @reason Use array lookup + * @author Spottedleaf + */ + @Overwrite + public boolean hasItem(final EntityDataAccessor entityDataAccessor) { + final int id = entityDataAccessor.getId(); + + return id >= 0 && id < this.itemsByArray.length && this.itemsByArray[id] != null; + } + + /** + * @reason Remove unnecessary locking, and use the array lookup + * @author Spottedleaf + */ + @Overwrite + private SynchedEntityData.DataItem getItem(final EntityDataAccessor entityDataAccessor) { + final int id = entityDataAccessor.getId(); + + if (id < 0 || id >= this.itemsByArray.length) { + return null; + } + + return (SynchedEntityData.DataItem)this.itemsByArray[id]; + } + + /** + * @reason Remove unnecessary locking, and use the array lookup + * @author Spottedleaf + */ + @Overwrite + public List> packDirty() { + if (!this.isDirty) { + return null; + } + this.isDirty = false; + + final List> ret = new ArrayList<>(); + + for (final SynchedEntityData.DataItem dataItem : this.itemsByArray) { + if (dataItem == null || !dataItem.isDirty()) { + continue; + } + dataItem.setDirty(false); + ret.add(dataItem.value()); + } + + return ret; + } + + /** + * @reason Remove unnecessary locking, and use the array lookup + * @author Spottedleaf + */ + @Overwrite + public List> getNonDefaultValues() { + List> ret = null; + for (final SynchedEntityData.DataItem dataItem : this.itemsByArray) { + if (dataItem == null || dataItem.isSetToDefault()) { + continue; + } + + if (ret == null) { + ret = new ArrayList<>(); + ret.add(dataItem.value()); + continue; + } else { + ret.add(dataItem.value()); + continue; + } + } + + return ret; + } + + /** + * @reason Remove unnecessary locking, and use the array lookup + * @author Spottedleaf + */ + @Overwrite + public void assignValues(final List> list) { + final SynchedEntityData.DataItem[] items = this.itemsByArray; + final int itemsLen = items.length; + for (int i = 0, len = list.size(); i < len; ++i) { + final SynchedEntityData.DataValue value = list.get(i); + final int valueId = value.id(); + + final SynchedEntityData.DataItem dataItem; + if (valueId < 0 || valueId >= itemsLen || (dataItem = items[valueId]) == null) { + continue; + } + + final EntityDataAccessor accessor = dataItem.getAccessor(); + this.assignValue(dataItem, value); + this.entity.onSyncedDataUpdated(accessor); + } + + this.entity.onSyncedDataUpdated(list); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/explosions/ExplosionMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/explosions/ExplosionMixin.java new file mode 100644 index 0000000..c2ca154 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/explosions/ExplosionMixin.java @@ -0,0 +1,493 @@ +package ca.spottedleaf.moonrise.mixin.explosions; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.patches.chunk_getblock.GetBlockChunk; +import ca.spottedleaf.moonrise.patches.collisions.CollisionUtil; +import ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState; +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; +import ca.spottedleaf.moonrise.patches.explosions.ExplosionBlockCache; +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.item.PrimedTnt; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.enchantment.ProtectionEnchantment; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.ExplosionDamageCalculator; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Mixin(Explosion.class) +public abstract class ExplosionMixin { + + @Shadow + @Final + private Level level; + + @Shadow + @Final + private Entity source; + + @Shadow + @Final + private double x; + + @Shadow + @Final + private double y; + + @Shadow + @Final + private double z; + + @Shadow + @Final + private ExplosionDamageCalculator damageCalculator; + + @Shadow + @Final + private float radius; + + @Shadow + @Final + private ObjectArrayList toBlow; + + @Shadow + public abstract DamageSource getDamageSource(); + + @Shadow + @Final + private Map hitPlayers; + + @Shadow + @Final + private boolean fire; + + + + @Unique + private static final double[] CACHED_RAYS; + static { + final DoubleArrayList rayCoords = new DoubleArrayList(); + + for (int x = 0; x <= 15; ++x) { + for (int y = 0; y <= 15; ++y) { + for (int z = 0; z <= 15; ++z) { + if ((x == 0 || x == 15) || (y == 0 || y == 15) || (z == 0 || z == 15)) { + double xDir = (double)((float)x / 15.0F * 2.0F - 1.0F); + double yDir = (double)((float)y / 15.0F * 2.0F - 1.0F); + double zDir = (double)((float)z / 15.0F * 2.0F - 1.0F); + + double mag = Math.sqrt( + xDir * xDir + yDir * yDir + zDir * zDir + ); + + rayCoords.add((xDir / mag) * (double)0.3F); + rayCoords.add((yDir / mag) * (double)0.3F); + rayCoords.add((zDir / mag) * (double)0.3F); + } + } + } + } + + CACHED_RAYS = rayCoords.toDoubleArray(); + } + + @Unique + private static final int CHUNK_CACHE_SHIFT = 2; + @Unique + private static final int CHUNK_CACHE_MASK = (1 << CHUNK_CACHE_SHIFT) - 1; + @Unique + private static final int CHUNK_CACHE_WIDTH = 1 << CHUNK_CACHE_SHIFT; + + @Unique + private static final int BLOCK_EXPLOSION_CACHE_SHIFT = 3; + @Unique + private static final int BLOCK_EXPLOSION_CACHE_MASK = (1 << BLOCK_EXPLOSION_CACHE_SHIFT) - 1; + @Unique + private static final int BLOCK_EXPLOSION_CACHE_WIDTH = 1 << BLOCK_EXPLOSION_CACHE_SHIFT; + + // resistance = (res + 0.3F) * 0.3F; + // so for resistance = 0, we need res = -0.3F + @Unique + private static final Float ZERO_RESISTANCE = Float.valueOf(-0.3f); + + @Unique + private Long2ObjectOpenHashMap blockCache = new Long2ObjectOpenHashMap<>(); + + @Unique + private long[] chunkPosCache = null; + + @Unique + private LevelChunk[] chunkCache = null; + + @Unique + private ExplosionBlockCache getOrCacheExplosionBlock(final int x, final int y, final int z, + final long key, final boolean calculateResistance) { + ExplosionBlockCache ret = this.blockCache.get(key); + if (ret != null) { + return ret; + } + + BlockPos pos = new BlockPos(x, y, z); + + if (!this.level.isInWorldBounds(pos)) { + ret = new ExplosionBlockCache(key, pos, null, null, 0.0f, true); + } else { + LevelChunk chunk; + long chunkKey = CoordinateUtils.getChunkKey(x >> 4, z >> 4); + int chunkCacheKey = ((x >> 4) & CHUNK_CACHE_MASK) | (((z >> 4) << CHUNK_CACHE_SHIFT) & (CHUNK_CACHE_MASK << CHUNK_CACHE_SHIFT)); + if (this.chunkPosCache[chunkCacheKey] == chunkKey) { + chunk = this.chunkCache[chunkCacheKey]; + } else { + this.chunkPosCache[chunkCacheKey] = chunkKey; + this.chunkCache[chunkCacheKey] = chunk = this.level.getChunk(x >> 4, z >> 4); + } + + BlockState blockState = ((GetBlockChunk)chunk).getBlock(x, y, z); + FluidState fluidState = blockState.getFluidState(); + + Optional resistance = !calculateResistance ? Optional.empty() : this.damageCalculator.getBlockExplosionResistance((Explosion)(Object)this, this.level, pos, blockState, fluidState); + + ret = new ExplosionBlockCache( + key, pos, blockState, fluidState, + (resistance.orElse(ZERO_RESISTANCE).floatValue() + 0.3f) * 0.3f, + false + ); + } + + this.blockCache.put(key, ret); + + return ret; + } + + @Unique + private boolean clipsAnything(final Vec3 from, final Vec3 to, + final CollisionUtil.LazyEntityCollisionContext context, + final ExplosionBlockCache[] blockCache, + final BlockPos.MutableBlockPos currPos) { + // assume that context.delegated = false + final double adjX = CollisionUtil.COLLISION_EPSILON * (from.x - to.x); + final double adjY = CollisionUtil.COLLISION_EPSILON * (from.y - to.y); + final double adjZ = CollisionUtil.COLLISION_EPSILON * (from.z - to.z); + + if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) { + return false; + } + + final double toXAdj = to.x - adjX; + final double toYAdj = to.y - adjY; + final double toZAdj = to.z - adjZ; + final double fromXAdj = from.x + adjX; + final double fromYAdj = from.y + adjY; + final double fromZAdj = from.z + adjZ; + + int currX = Mth.floor(fromXAdj); + int currY = Mth.floor(fromYAdj); + int currZ = Mth.floor(fromZAdj); + + final double diffX = toXAdj - fromXAdj; + final double diffY = toYAdj - fromYAdj; + final double diffZ = toZAdj - fromZAdj; + + final double dxDouble = Math.signum(diffX); + final double dyDouble = Math.signum(diffY); + final double dzDouble = Math.signum(diffZ); + + final int dx = (int)dxDouble; + final int dy = (int)dyDouble; + final int dz = (int)dzDouble; + + final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX; + final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY; + final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ; + + double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj)); + double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj)); + double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj)); + + for (;;) { + currPos.set(currX, currY, currZ); + + // ClipContext.Block.COLLIDER -> BlockBehaviour.BlockStateBase::getCollisionShape + // ClipContext.Fluid.NONE -> ignore fluids + + // read block from cache + final long key = BlockPos.asLong(currX, currY, currZ); + + final int cacheKey = + (currX & BLOCK_EXPLOSION_CACHE_MASK) | + (currY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | + (currZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); + ExplosionBlockCache cachedBlock = blockCache[cacheKey]; + if (cachedBlock == null || cachedBlock.key != key) { + blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(currX, currY, currZ, key, false); + } + + final BlockState blockState = cachedBlock.blockState; + if (!((CollisionBlockState)blockState).emptyCollisionShape()) { + VoxelShape collision = cachedBlock.cachedCollisionShape; + if (collision == null) { + collision = blockState.getCollisionShape(this.level, currPos, context); + if (!context.isDelegated()) { + // if it was not delegated during this call, assume that for any future ones it will not be delegated + // again, and cache the result + cachedBlock.cachedCollisionShape = collision; + } + } + + if (!collision.isEmpty() && collision.clip(from, to, currPos) != null) { + return true; + } + } + + if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) { + return false; + } + + // inc the smallest normalized coordinate + + if (normalizedCurrX < normalizedCurrY) { + if (normalizedCurrX < normalizedCurrZ) { + currX += dx; + normalizedCurrX += normalizedDiffX; + } else { + // x < y && x >= z <--> z < y && z <= x + currZ += dz; + normalizedCurrZ += normalizedDiffZ; + } + } else if (normalizedCurrY < normalizedCurrZ) { + // y <= x && y < z + currY += dy; + normalizedCurrY += normalizedDiffY; + } else { + // y <= x && z <= y <--> z <= y && z <= x + currZ += dz; + normalizedCurrZ += normalizedDiffZ; + } + } + } + + @Unique + private double getSeenFraction(final Vec3 source, final Entity target, + final ExplosionBlockCache[] blockCache, + final BlockPos.MutableBlockPos blockPos) { + final AABB boundingBox = target.getBoundingBox(); + final double diffX = boundingBox.maxX - boundingBox.minX; + final double diffY = boundingBox.maxY - boundingBox.minY; + final double diffZ = boundingBox.maxZ - boundingBox.minZ; + + final double incX = 1.0 / (diffX * 2.0 + 1.0); + final double incY = 1.0 / (diffY * 2.0 + 1.0); + final double incZ = 1.0 / (diffZ * 2.0 + 1.0); + + if (incX < 0.0 || incY < 0.0 || incZ < 0.0) { + return 0.0; + } + + final double offX = (1.0 - Math.floor(1.0 / incX) * incX) * 0.5 + boundingBox.minX; + final double offY = boundingBox.minY; + final double offZ = (1.0 - Math.floor(1.0 / incZ) * incZ) * 0.5 + boundingBox.minZ; + + final CollisionUtil.LazyEntityCollisionContext context = new CollisionUtil.LazyEntityCollisionContext(target); + + int totalRays = 0; + int missedRays = 0; + + for (double dx = 0.0; dx <= 1.0; dx += incX) { + final double fromX = Math.fma(dx, diffX, offX); + for (double dy = 0.0; dy <= 1.0; dy += incY) { + final double fromY = Math.fma(dy, diffY, offY); + for (double dz = 0.0; dz <= 1.0; dz += incZ) { + ++totalRays; + + final Vec3 from = new Vec3( + fromX, + fromY, + Math.fma(dz, diffZ, offZ) + ); + + if (!this.clipsAnything(from, source, context, blockCache, blockPos)) { + ++missedRays; + } + } + } + } + + return (double)missedRays / (double)totalRays; + } + + /** + * @reason Rewrite ray casting and seen fraction calculation for performance + * @author Spottedleaf + */ + @Overwrite + public void explode() { + this.level.gameEvent(this.source, GameEvent.EXPLODE, new Vec3(this.x, this.y, this.z)); + + this.chunkPosCache = new long[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; + Arrays.fill(this.chunkPosCache, ChunkPos.INVALID_CHUNK_POS); + + this.chunkCache = new LevelChunk[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; + + ExplosionBlockCache[] blockCache = new ExplosionBlockCache[BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH]; + // avoid checking for initial state in loop by always defaulting to a position that will fail the first cache check + ExplosionBlockCache initialCache = new ExplosionBlockCache( + BlockPos.containing(this.x, this.y, this.z).above(2).asLong(), + null, null, null, 0f, true + ); + ExplosionBlockCache cachedBlock; + + // only ~1/3rd of the loop iterations in vanilla will result in a ray, as it is iterating the perimeter of + // a 16x16x16 cube + // we can cache the rays and their normals as well, so that we eliminate the excess iterations / checks and + // calculations in one go + // additional aggressive caching of block retrieval is very significant, as at low power (i.e tnt) most + // block retrievals are not unique + for (int ray = 0, len = CACHED_RAYS.length; ray < len;) { + cachedBlock = initialCache; + + double currX = this.x; + double currY = this.y; + double currZ = this.z; + + double incX = CACHED_RAYS[ray]; + double incY = CACHED_RAYS[ray + 1]; + double incZ = CACHED_RAYS[ray + 2]; + + ray += 3; + + float power = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F); + + do { + int blockX = Mth.floor(currX); + int blockY = Mth.floor(currY); + int blockZ = Mth.floor(currZ); + + final long key = BlockPos.asLong(blockX, blockY, blockZ); + + if (cachedBlock.key != key) { + final int cacheKey = + (blockX & BLOCK_EXPLOSION_CACHE_MASK) | + (blockY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | + (blockZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); + cachedBlock = blockCache[cacheKey]; + if (cachedBlock == null || cachedBlock.key != key) { + blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); + } + } + + if (cachedBlock.outOfWorld) { + break; + } + + power -= cachedBlock.resistance; + + if (power > 0.0f && cachedBlock.shouldExplode == null) { + // note: we expect shouldBlockExplode to be pure with respect to power, as Vanilla currently is. + // basically, it is unused, which allows us to cache the result + final boolean shouldExplode = this.damageCalculator.shouldBlockExplode((Explosion)(Object)this, this.level, cachedBlock.immutablePos, cachedBlock.blockState, power); + cachedBlock.shouldExplode = shouldExplode ? Boolean.TRUE : Boolean.FALSE; + if (shouldExplode) { + if (this.fire || !cachedBlock.blockState.isAir()) { + this.toBlow.add(cachedBlock.immutablePos); + } + } + } + + power -= 0.22500001F; + currX += incX; + currY += incY; + currZ += incZ; + } while (power > 0.0f); + } + + final double diameter = (double)this.radius * 2.0; + // use null predicate to avoid indirection on test(), but we need to move the spectator check into the loop itself + final List entities = this.level.getEntities(this.source, + new AABB( + Mth.floor(this.x - (diameter + 1.0)), + Mth.floor(this.y - (diameter + 1.0)), + Mth.floor(this.z - (diameter + 1.0)), + + Mth.floor(this.x + (diameter + 1.0)), + Mth.floor(this.y + (diameter + 1.0)), + Mth.floor(this.z + (diameter + 1.0)) + ), + null + ); + final Vec3 center = new Vec3(this.x, this.y, this.z); + + final BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos(); + + for (int i = 0, len = entities.size(); i < len; ++i) { + final Entity entity = entities.get(i); + if (entity.isSpectator() || entity.ignoreExplosion()) { + continue; + } + + final double normalizedDistanceToCenter = Math.sqrt(entity.distanceToSqr(center)) / diameter; + if (normalizedDistanceToCenter > 1.0) { + continue; + } + + double distX = entity.getX() - this.x; + double distY = (entity instanceof PrimedTnt ? entity.getY() : entity.getEyeY()) - this.y; + double distZ = entity.getZ() - this.z; + final double distMag = Math.sqrt(distX * distX + distY * distY + distZ * distZ); + + if (distMag != 0.0) { + distX /= distMag; + distY /= distMag; + distZ /= distMag; + + // route to new visible fraction calculation, using the existing block cache + final double visibleFraction = this.getSeenFraction(center, entity, blockCache, blockPos); + final double intensityFraction = (1.0 - normalizedDistanceToCenter) * visibleFraction; + + entity.hurt(this.getDamageSource(), (float)((int)((intensityFraction * intensityFraction + intensityFraction) / 2.0 * 7.0 * diameter + 1.0))); + + final double knockbackFraction; + if (entity instanceof LivingEntity livingEntity) { + knockbackFraction = ProtectionEnchantment.getExplosionKnockbackAfterDampener(livingEntity, intensityFraction); + } else { + knockbackFraction = intensityFraction; + } + + final Vec3 knockback = new Vec3(distX * knockbackFraction, distY * knockbackFraction, distZ * knockbackFraction); + entity.setDeltaMovement(entity.getDeltaMovement().add(knockback)); + + if (entity instanceof Player player) { + if (!player.isSpectator() && (!player.isCreative() || !player.getAbilities().flying)) { + this.hitPlayers.put(player, knockback); + } + } + } + } + + this.blockCache = null; + this.chunkPosCache = null; + this.chunkCache = null; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/explosions/ExplosionProfileMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/explosions/ExplosionProfileMixin.java new file mode 100644 index 0000000..7bfc1b4 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/explosions/ExplosionProfileMixin.java @@ -0,0 +1,41 @@ +package ca.spottedleaf.moonrise.mixin.explosions; + +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(Level.class) +public abstract class ExplosionProfileMixin implements LevelAccessor, AutoCloseable { + + @Unique + private long time; + @Unique + private int count; + + @Redirect( + method = "explode(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/damagesource/DamageSource;Lnet/minecraft/world/level/ExplosionDamageCalculator;DDDFZLnet/minecraft/world/level/Level$ExplosionInteraction;Z)Lnet/minecraft/world/level/Explosion;", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/Explosion;explode()V" + ) + ) + private void aa(Explosion instance) { + long start = System.nanoTime(); + instance.explode(); + long end = System.nanoTime(); + ++this.count; + this.time += (end - start); + } + + @Overwrite + @Override + public void close() throws Exception { + this.getChunkSource().close(); + System.out.println("expl: count: " + this.count + ", time: " + this.time + ", ms per explode: " + (double)this.time / (double)this.count * 1.0E-6); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/fluid/FlowingFluidMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/fluid/FlowingFluidMixin.java new file mode 100644 index 0000000..dd8922e --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/fluid/FlowingFluidMixin.java @@ -0,0 +1,624 @@ +package ca.spottedleaf.moonrise.mixin.fluid; + +import ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState; +import ca.spottedleaf.moonrise.patches.collisions.util.CollisionDirection; +import ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey; +import ca.spottedleaf.moonrise.patches.fluids.FluidClassification; +import ca.spottedleaf.moonrise.patches.fluids.FluidFluid; +import ca.spottedleaf.moonrise.patches.fluids.FluidFluidState; +import com.google.common.collect.Maps; +import it.unimi.dsi.fastutil.shorts.Short2ByteOpenHashMap; +import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.IntegerProperty; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.material.FlowingFluid; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@Mixin(FlowingFluid.class) +public abstract class FlowingFluidMixin extends Fluid { + + @Shadow + protected abstract int getDropOff(LevelReader levelReader); + + @Shadow + public abstract Fluid getSource(); + + @Shadow + @Final + public static BooleanProperty FALLING; + + @Shadow + @Final + public static IntegerProperty LEVEL; + + @Shadow + protected abstract boolean canConvertToSource(Level level); + + @Shadow + public abstract Fluid getFlowing(); + + @Shadow + protected abstract void spreadTo(LevelAccessor levelAccessor, BlockPos blockPos, BlockState blockState, Direction direction, FluidState fluidState); + + @Shadow + protected abstract boolean canHoldFluid(BlockGetter blockGetter, BlockPos blockPos, BlockState blockState, Fluid fluid); + + @Shadow + protected abstract int getSlopeFindDistance(LevelReader levelReader); + + + @Shadow + public static short getCacheKey(BlockPos blockPos, BlockPos blockPos2) { + return (short)0; + } + + + @Unique + private FluidState sourceFalling; + + @Unique + private FluidState sourceNotFalling; + + @Unique + private static final int TOTAL_FLOWING_STATES = FALLING.getPossibleValues().size() * LEVEL.getPossibleValues().size(); + + @Unique + private static final int MIN_LEVEL = LEVEL.getPossibleValues().stream().sorted().findFirst().get().intValue(); + + // index = (falling ? 1 : 0) + level*2 + @Unique + private FluidState[] flowingLookUp; + + @Unique + private volatile boolean init; + + /** + * Due to init order, we need to use callbacks to initialise our state + */ + @Unique + private void init() { + for (final FluidState state : this.getFlowing().getStateDefinition().getPossibleStates()) { + if (!state.isSource()) { + if (this.flowingLookUp == null) { + this.flowingLookUp = new FluidState[TOTAL_FLOWING_STATES]; + } + final int index = (state.getValue(FALLING).booleanValue() ? 1 : 0) | ((state.getValue(LEVEL).intValue() - MIN_LEVEL) << 1); + if (this.flowingLookUp[index] != null) { + throw new IllegalStateException("Already inited"); + } + this.flowingLookUp[index] = state; + } + } + for (final FluidState state : this.getSource().getStateDefinition().getPossibleStates()) { + if (state.isSource()) { + if (state.getValue(FALLING).booleanValue()) { + if (this.sourceFalling != null) { + throw new IllegalStateException("Already inited"); + } + this.sourceFalling = state; + } else { + if (this.sourceNotFalling != null) { + throw new IllegalStateException("Already inited"); + } + this.sourceNotFalling = state; + } + } + } + + this.init = true; + } + + /** + * @reason Use cached result to avoid indirection + * @author Spottedleaf + */ + @Overwrite + public FluidState getSource(final boolean falling) { + if (!this.init) { + this.init(); + } + return falling ? this.sourceFalling : this.sourceNotFalling; + } + + /** + * @reason Use cached result to avoid indirection + * @author Spottedleaf + */ + @Overwrite + public FluidState getFlowing(final int amount, final boolean falling) { + if (!this.init) { + this.init(); + } + final int index = (falling ? 1 : 0) | ((amount - MIN_LEVEL) << 1); + return this.flowingLookUp[index]; + } + + + @Unique + private static final int COLLISION_OCCLUSION_CACHE_SIZE = 2048; + + @Unique + private static final FluidOcclusionCacheKey[] COLLISION_OCCLUSION_CACHE = new FluidOcclusionCacheKey[COLLISION_OCCLUSION_CACHE_SIZE]; + + /** + * @reason Try to avoid going to the cache for simple cases; additionally use better caching strategy + * @author Spottedleaf + */ + @Overwrite + private boolean canPassThroughWall(final Direction direction, final BlockGetter level, + final BlockPos fromPos, final BlockState fromState, + final BlockPos toPos, final BlockState toState) { + if (((CollisionBlockState)fromState).emptyCollisionShape() & ((CollisionBlockState)toState).emptyCollisionShape()) { + // don't even try to cache simple cases + return true; + } + + if (((CollisionBlockState)fromState).occludesFullBlock() | ((CollisionBlockState)toState).occludesFullBlock()) { + // don't even try to cache simple cases + return false; + } + + final FluidOcclusionCacheKey[] cache = ((CollisionBlockState)fromState).hasCache() & ((CollisionBlockState)toState).hasCache() ? + COLLISION_OCCLUSION_CACHE : null; + + final int keyIndex + = (((CollisionBlockState)fromState).uniqueId1() ^ ((CollisionBlockState)toState).uniqueId2() ^ ((CollisionDirection)(Object)direction).uniqueId()) + & (COLLISION_OCCLUSION_CACHE_SIZE - 1); + + if (cache != null) { + final FluidOcclusionCacheKey cached = cache[keyIndex]; + if (cached != null && cached.first() == fromState && cached.second() == toState && cached.direction() == direction) { + return cached.result(); + } + } + + final VoxelShape shape1 = fromState.getCollisionShape(level, fromPos); + final VoxelShape shape2 = toState.getCollisionShape(level, toPos); + + final boolean result = !Shapes.mergedFaceOccludes(shape1, shape2, direction); + + if (cache != null) { + // we can afford to replace in-use keys more often due to the excessive caching the collision patch does in mergedFaceOccludes + cache[keyIndex] = new FluidOcclusionCacheKey(fromState, toState, direction, result); + } + + return result; + } + + @Unique + private static final Direction[] HORIZONTAL_ARRAY = Direction.Plane.HORIZONTAL.stream().toList().toArray(new Direction[0]); + + @Unique + private static final FluidState EMPTY_FLUID_STATE = Fluids.EMPTY.defaultFluidState(); + + /** + * @reason Optimise method + * @author Spottedleaf + */ + @Overwrite + public FluidState getNewLiquid(final Level level, final BlockPos fromPos, final BlockState fromState) { + final FluidClassification thisClassification = ((FluidFluid)this).getClassification(); + + LevelChunk lastChunk = null; + int lastChunkX = Integer.MIN_VALUE; + int lastChunkZ = Integer.MIN_VALUE; + + int newAmount = 0; + int nearbySources = 0; + + final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos(); + + for (final Direction direction : HORIZONTAL_ARRAY) { + tempPos.set(fromPos.getX() + direction.getStepX(), fromPos.getY(), fromPos.getZ() + direction.getStepZ()); + + final int newChunkX = tempPos.getX() >> 4; + final int newChunkZ = tempPos.getZ() >> 4; + + final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); + + if (chunkDiff != 0) { + lastChunk = level.getChunk(newChunkX, newChunkZ); + + lastChunkX = newChunkX; + lastChunkZ = newChunkZ; + } + + final BlockState neighbourState = lastChunk.getBlockState(tempPos); + final FluidState fluidState = neighbourState.getFluidState(); + + if ((((FluidFluidState)(Object)fluidState).getClassification() == thisClassification) && this.canPassThroughWall(direction, level, fromPos, fromState, tempPos, neighbourState)) { + if (fluidState.isSource()) { + ++nearbySources; + } + + newAmount = Math.max(newAmount, fluidState.getAmount()); + } + } + + tempPos.set(fromPos.getX(), fromPos.getY() - 1, fromPos.getZ()); + final int newChunkX = tempPos.getX() >> 4; + final int newChunkZ = tempPos.getZ() >> 4; + + final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); + + if (chunkDiff != 0) { + lastChunk = level.getChunk(newChunkX, newChunkZ); + + lastChunkX = newChunkX; + lastChunkZ = newChunkZ; + } + + if (nearbySources >= 2 && this.canConvertToSource(level)) { + final BlockState belowState = lastChunk.getBlockState(tempPos); + final FluidState belowFluid = belowState.getFluidState(); + + if (belowState.isSolid() || (belowFluid.isSource() && ((FluidFluidState)(Object)belowFluid).getClassification() == thisClassification)) { + return this.getSource(false); + } + } + + tempPos.setY(fromPos.getY() + 1); + final BlockState aboveState = lastChunk.getBlockState(tempPos); + final FluidState aboveFluid = aboveState.getFluidState(); + + // drop empty check, we cannot be empty + if ((((FluidFluidState)(Object)aboveFluid).getClassification() == thisClassification) && this.canPassThroughWall(Direction.UP, level, fromPos, fromState, tempPos, aboveState)) { + return this.getFlowing(8, true); + } else { + final int finalAmount = newAmount - this.getDropOff(level); + return finalAmount <= 0 ? EMPTY_FLUID_STATE : this.getFlowing(finalAmount, false); + } + } + + /** + * @reason Optimise + * @author Spottedleaf + */ + @Overwrite + public void spread(final Level level, final BlockPos fromPos, final FluidState fromFluid) { + if (fromFluid.isEmpty()) { + return; + } + + final LevelChunk fromChunk = level.getChunk(fromPos.getX() >> 4, fromPos.getZ() >> 4); + final BlockState fromState = fromChunk.getBlockState(fromPos); + final BlockPos belowPos = fromPos.below(); + final BlockState belowState = fromChunk.getBlockState(belowPos); + final FluidState belowFluid = belowState.getFluidState(); + final FluidState newFluid = this.getNewLiquid(level, belowPos, belowState); + + if (this.canSpreadTo(level, fromPos, fromState, Direction.DOWN, belowPos, belowState, belowFluid, newFluid.getType())) { + this.spreadTo(level, belowPos, belowState, Direction.DOWN, newFluid); + if (this.sourceNeighborCount(level, fromPos) >= 3) { + this.spreadToSides(level, fromPos, fromFluid, fromState); + } + } else if (fromFluid.isSource() || !this.isWaterHole(level, newFluid.getType(), fromPos, fromState, belowPos, belowState)) { + this.spreadToSides(level, fromPos, fromFluid, fromState); + } + } + + /** + * @reason Optimise + * @author Spottedleaf + */ + @Overwrite + private int sourceNeighborCount(final LevelReader level, final BlockPos fromPos) { + final FluidClassification thisClassification = ((FluidFluid)this).getClassification(); + + ChunkAccess lastChunk = null; + int lastChunkX = Integer.MIN_VALUE; + int lastChunkZ = Integer.MIN_VALUE; + + int ret = 0; + + final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos(); + + for (final Direction direction : HORIZONTAL_ARRAY) { + tempPos.set(fromPos.getX() + direction.getStepX(), fromPos.getY(), fromPos.getZ() + direction.getStepZ()); + + final int newChunkX = tempPos.getX() >> 4; + final int newChunkZ = tempPos.getZ() >> 4; + + final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); + + if (chunkDiff != 0) { + lastChunk = level.getChunk(newChunkX, newChunkZ); + + lastChunkX = newChunkX; + lastChunkZ = newChunkZ; + } + + FluidState fluidState = lastChunk.getBlockState(tempPos).getFluidState(); + if (fluidState.isSource() && (((FluidFluidState)(Object)fluidState).getClassification() == thisClassification)) { + ++ret; + } + } + + return ret; + } + + @Unique + private static final byte UNCACHED_RESULT = (byte)-1; + + /** + * @reason Optimise + * @author Spottedleaf + */ + @Overwrite + public Map getSpread(final Level level, final BlockPos fromPos, final BlockState fromState) { + ChunkAccess lastChunk = null; + int lastChunkX = Integer.MIN_VALUE; + int lastChunkZ = Integer.MIN_VALUE; + + int minSlope = 1000; + + final Map ret = Maps.newEnumMap(Direction.class); + final Short2ObjectOpenHashMap blockLookupCache = new Short2ObjectOpenHashMap<>(); + final Short2ByteOpenHashMap waterHoleCache = new Short2ByteOpenHashMap(); + waterHoleCache.defaultReturnValue(UNCACHED_RESULT); + + final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos(); + + for (final Direction direction : HORIZONTAL_ARRAY) { + tempPos.set(fromPos.getX() + direction.getStepX(), fromPos.getY(), fromPos.getZ() + direction.getStepZ()); + final short cacheKey = getCacheKey(fromPos, tempPos); + + BlockState neighbourState = blockLookupCache.get(cacheKey); + if (neighbourState == null) { + final int newChunkX = tempPos.getX() >> 4; + final int newChunkZ = tempPos.getZ() >> 4; + + final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); + + if (chunkDiff != 0) { + lastChunk = level.getChunk(newChunkX, newChunkZ); + + lastChunkX = newChunkX; + lastChunkZ = newChunkZ; + } + neighbourState = lastChunk.getBlockState(tempPos); + blockLookupCache.put(cacheKey, neighbourState); + } + + final FluidState neighbourFluid = neighbourState.getFluidState(); + final FluidState newNeighbourFluid = this.getNewLiquid(level, tempPos, neighbourState); + + if (this.canPassThrough(level, newNeighbourFluid.getType(), fromPos, fromState, direction, tempPos, neighbourState, neighbourFluid)) { + byte isWaterHole = waterHoleCache.get(cacheKey); + if (isWaterHole == UNCACHED_RESULT) { + final int newChunkX = tempPos.getX() >> 4; + final int newChunkZ = tempPos.getZ() >> 4; + + final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); + + if (chunkDiff != 0) { + lastChunk = level.getChunk(newChunkX, newChunkZ); + + lastChunkX = newChunkX; + lastChunkZ = newChunkZ; + } + final BlockPos neighbourBelowPos = tempPos.below(); + BlockState belowState = lastChunk.getBlockState(neighbourBelowPos); + + isWaterHole = this.isWaterHole(level, this.getFlowing(), tempPos, neighbourState, neighbourBelowPos, belowState) ? (byte)1 : (byte)0; + waterHoleCache.put(cacheKey, isWaterHole); + } + + final int slopeDistance = isWaterHole == (byte)1 ? 0 : this.getSlopeDistanceOptimised( + level, tempPos, 1, direction.getOpposite(), neighbourState, fromPos, blockLookupCache, waterHoleCache, + lastChunk, lastChunkX, lastChunkZ); + + if (slopeDistance < minSlope) { + ret.clear(); + } + + if (slopeDistance <= minSlope) { + ret.put(direction, newNeighbourFluid); + minSlope = slopeDistance; + } + } + } + + return ret; + } + + @Unique + private static final Direction[][] HORIZONTAL_EXCEPT; + static { + final Direction[][] except = new Direction[Direction.values().length][]; + for (final Direction direction : HORIZONTAL_ARRAY) { + final List directionsWithout = new ArrayList<>(Arrays.asList(HORIZONTAL_ARRAY)); + directionsWithout.remove(direction); + except[direction.ordinal()] = directionsWithout.toArray(new Direction[0]); + } + HORIZONTAL_EXCEPT = except; + } + + @Unique + private int getSlopeDistanceOptimised(final LevelReader level, final BlockPos fromPos, final int step, final Direction fromDirection, + final BlockState fromState, final BlockPos originPos, + final Short2ObjectOpenHashMap blockLookupCache, + final Short2ByteOpenHashMap belowIsWaterHole, + ChunkAccess lastChunk, int lastChunkX, int lastChunkZ) { + final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos(); + final Fluid flowing = this.getFlowing(); + + int ret = 1000; + + for (final Direction direction : HORIZONTAL_EXCEPT[fromDirection.ordinal()]) { + tempPos.set(fromPos.getX() + direction.getStepX(), fromPos.getY(), fromPos.getZ() + direction.getStepZ()); + final short cacheKey = getCacheKey(originPos, tempPos); + + BlockState neighbourState = blockLookupCache.get(cacheKey); + if (neighbourState == null) { + final int newChunkX = tempPos.getX() >> 4; + final int newChunkZ = tempPos.getZ() >> 4; + + final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); + + if (chunkDiff != 0) { + lastChunk = level.getChunk(newChunkX, newChunkZ); + + lastChunkX = newChunkX; + lastChunkZ = newChunkZ; + } + neighbourState = lastChunk.getBlockState(tempPos); + blockLookupCache.put(cacheKey, neighbourState); + } + + FluidState neighbourFluid = neighbourState.getFluidState(); + if (this.canPassThrough(level, flowing, fromPos, fromState, direction, tempPos, neighbourState, neighbourFluid)) { + byte isWaterHole = belowIsWaterHole.get(cacheKey); + if (isWaterHole == UNCACHED_RESULT) { + final int newChunkX = tempPos.getX() >> 4; + final int newChunkZ = tempPos.getZ() >> 4; + + final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); + + if (chunkDiff != 0) { + lastChunk = level.getChunk(newChunkX, newChunkZ); + + lastChunkX = newChunkX; + lastChunkZ = newChunkZ; + } + + final BlockPos belowPos = tempPos.below(); + final BlockState belowState = lastChunk.getBlockState(belowPos); + isWaterHole = this.isWaterHole(level, flowing, tempPos, neighbourState, belowPos, belowState) ? (byte)1 : (byte)0; + belowIsWaterHole.put(cacheKey, isWaterHole); + } + if (isWaterHole == (byte)1) { + return step; + } + + if (step < this.getSlopeFindDistance(level)) { + final int slopeNeighbour = this.getSlopeDistanceOptimised( + level, tempPos, step + 1, direction.getOpposite(), neighbourState, originPos, blockLookupCache, belowIsWaterHole, + lastChunk, lastChunkX, lastChunkZ + ); + ret = Math.min(slopeNeighbour, ret); + } + } + } + + return ret; + } + + /** + * @reason Avoid indirection for empty/air case + * @author Spottedleaf + */ + @Overwrite + public boolean canSpreadTo(final BlockGetter level, + final BlockPos fromPos, final BlockState fromState, final Direction direction, + final BlockPos toPos, final BlockState toState, final FluidState toFluid, final Fluid toType) { + return (toFluid.isEmpty() || toFluid.canBeReplacedWith(level, toPos, toType, direction)) && + this.canPassThroughWall(direction, level, fromPos, fromState, toPos, toState) && + (toState.isAir() || this.canHoldFluid(level, toPos, toState, toType)); + } + + /** + * @reason Optimise + * @author Spottedleaf + */ + @Overwrite + private void spreadToSides(final Level level, final BlockPos fromPos, final FluidState fromFluid, final BlockState fromState) { + final int amount; + if (fromFluid.getValue(FALLING).booleanValue()) { + amount = 7; + } else { + amount = fromFluid.getAmount() - this.getDropOff(level); + } + + if (amount <= 0) { + return; + } + + ChunkAccess lastChunk = null; + int lastChunkX = Integer.MIN_VALUE; + int lastChunkZ = Integer.MIN_VALUE; + + final Map spread = this.getSpread(level, fromPos, fromState); + final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos(); + + for (final Map.Entry entry : spread.entrySet()) { + final Direction direction = entry.getKey(); + final FluidState newFluid = entry.getValue(); + + tempPos.set(fromPos.getX() + direction.getStepX(), fromPos.getY() + direction.getStepY(), fromPos.getZ() + direction.getStepZ()); + + final int newChunkX = tempPos.getX() >> 4; + final int newChunkZ = tempPos.getZ() >> 4; + + final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); + + if (chunkDiff != 0) { + lastChunk = level.getChunk(newChunkX, newChunkZ); + + lastChunkX = newChunkX; + lastChunkZ = newChunkZ; + } + + final BlockState currentState = lastChunk.getBlockState(tempPos); + final FluidState currentFluid = currentState.getFluidState(); + if (this.canSpreadTo(level, fromPos, fromState, direction, tempPos, currentState, currentFluid, newFluid.getType())) { + this.spreadTo(level, tempPos, currentState, direction, newFluid); + } + } + } + + /** + * @reason Optimise + * @author Spottedleaf + */ + @Overwrite + private boolean isWaterHole(final BlockGetter level, final Fluid type, + final BlockPos fromPos, final BlockState fromState, + final BlockPos toPos, final BlockState toState) { + final FluidClassification classification = ((FluidFluid)this).getClassification(); + final FluidClassification otherClassification = ((FluidFluidState)(Object)toState.getFluidState()).getClassification(); + return this.canPassThroughWall(Direction.DOWN, level, fromPos, fromState, toPos, toState) && + ( + (otherClassification == classification) + || (toState.isAir() || this.canHoldFluid(level, toPos, toState, type)) + ); + } + + /** + * @reason Optimise + * @author Spottedleaf + */ + @Overwrite + private boolean canPassThrough(final BlockGetter level, final Fluid type, + final BlockPos fromPos, final BlockState fromState, final Direction direction, + final BlockPos toPos, final BlockState toState, final FluidState toFluid) { + final FluidClassification classification = ((FluidFluid)this).getClassification(); + final FluidClassification otherClassification = ((FluidFluidState)(Object)toFluid).getClassification(); + + return (!toFluid.isSource() || classification != otherClassification) && + this.canPassThroughWall(direction, level, fromPos, fromState, toPos, toState) && + (toState.isAir() || this.canHoldFluid(level, toPos, toState, type)); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/fluid/FluidMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/fluid/FluidMixin.java new file mode 100644 index 0000000..7a6c4b7 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/fluid/FluidMixin.java @@ -0,0 +1,54 @@ +package ca.spottedleaf.moonrise.mixin.fluid; + +import ca.spottedleaf.moonrise.patches.fluids.FluidClassification; +import ca.spottedleaf.moonrise.patches.fluids.FluidFluid; +import com.mojang.logging.LogUtils; +import net.minecraft.world.level.material.EmptyFluid; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.LavaFluid; +import net.minecraft.world.level.material.WaterFluid; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Fluid.class) +public abstract class FluidMixin implements FluidFluid { + + @Unique + private static final Logger LOGGER = LogUtils.getLogger(); + + @Unique + private FluidClassification classification; + + /** + * @reason Init caches + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void init(final CallbackInfo ci) { + if ((Object)this instanceof EmptyFluid) { + this.classification = FluidClassification.EMPTY; + } else if ((Object)this instanceof LavaFluid) { + this.classification = FluidClassification.LAVA; + } else if ((Object)this instanceof WaterFluid) { + this.classification = FluidClassification.WATER; + } + + if (this.classification == null) { + LOGGER.error("Unknown fluid classification " + this.getClass().getName()); + } + } + + @Override + public FluidClassification getClassification() { + return this.classification; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/fluid/FluidStateMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/fluid/FluidStateMixin.java new file mode 100644 index 0000000..9a01b45 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/fluid/FluidStateMixin.java @@ -0,0 +1,147 @@ +package ca.spottedleaf.moonrise.mixin.fluid; + +import ca.spottedleaf.moonrise.patches.fluids.FluidClassification; +import ca.spottedleaf.moonrise.patches.fluids.FluidFluidState; +import com.google.common.collect.ImmutableMap; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.MapCodec; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateHolder; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.material.EmptyFluid; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.LavaFluid; +import net.minecraft.world.level.material.WaterFluid; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(FluidState.class) +public abstract class FluidStateMixin extends StateHolder implements FluidFluidState { + + @Shadow + public abstract Fluid getType(); + + protected FluidStateMixin(Fluid object, ImmutableMap, Comparable> immutableMap, MapCodec mapCodec) { + super(object, immutableMap, mapCodec); + } + + @Unique + private static final Logger LOGGER = LogUtils.getLogger(); + + @Unique + private int amount; + + @Unique + private boolean isEmpty; + + @Unique + private boolean isSource; + + @Unique + private float ownHeight; + + @Unique + private BlockState legacyBlock; + + @Unique + private FluidClassification classification; + + /** + * @reason Initialise caches + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void init(final CallbackInfo ci) { + this.amount = this.getType().getAmount((FluidState)(Object)this); + this.isEmpty = this.getType().isEmpty(); + this.isSource = this.getType().isSource((FluidState)(Object)this); + this.ownHeight = this.getType().getOwnHeight((FluidState)(Object)this); + + if (this.getType() instanceof EmptyFluid) { + this.classification = FluidClassification.EMPTY; + } else if (this.getType() instanceof LavaFluid) { + this.classification = FluidClassification.LAVA; + } else if (this.getType() instanceof WaterFluid) { + this.classification = FluidClassification.WATER; + } + + if (this.classification == null) { + LOGGER.error("Unknown fluid classification " + this.getClass().getName()); + } + } + + + /** + * @reason Use cached result, avoiding indirection + * @author Spottedleaf + */ + @Overwrite + public int getAmount() { + return this.amount; + } + + /** + * @reason Use cached result, avoiding indirection + * @author Spottedleaf + */ + @Overwrite + public boolean isEmpty() { + return this.isEmpty; + } + + /** + * @reason Use cached result, avoiding indirection + * @author Spottedleaf + */ + @Overwrite + public boolean isSource() { + return this.isSource; + } + + /** + * @reason Use cached result, avoiding indirection + * @author Spottedleaf + */ + @Overwrite + public float getOwnHeight() { + return this.ownHeight; + } + + /** + * @reason Use cached result, avoiding indirection + * @author Spottedleaf + */ + @Overwrite + public boolean isSourceOfType(final Fluid other) { + return this.isSource && this.owner == other; + } + + /** + * @reason Use cached result, avoiding indirection + * @author Spottedleaf + */ + @Overwrite + public BlockState createLegacyBlock() { + if (this.legacyBlock != null) { + return this.legacyBlock; + } + return this.legacyBlock = this.getType().createLegacyBlock((FluidState)(Object)this); + } + + @Override + public final FluidClassification getClassification() { + return this.classification; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/hopper/HopperBlockEntityMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/hopper/HopperBlockEntityMixin.java new file mode 100644 index 0000000..a7d2a77 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/hopper/HopperBlockEntityMixin.java @@ -0,0 +1,85 @@ +package ca.spottedleaf.moonrise.mixin.hopper; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.entity.Hopper; +import net.minecraft.world.level.block.entity.HopperBlockEntity; +import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Unique; +import java.util.List; + +@Mixin(HopperBlockEntity.class) +public abstract class HopperBlockEntityMixin extends RandomizableContainerBlockEntity implements Hopper { + protected HopperBlockEntityMixin(BlockEntityType blockEntityType, BlockPos blockPos, BlockState blockState) { + super(blockEntityType, blockPos, blockState); + } + + @Unique + private static final AABB HOPPER_ITEM_SUCK_OVERALL = Hopper.SUCK.bounds(); + @Unique + private static final AABB[] HOPPER_ITEM_SUCK_INDIVIDUAL = Hopper.SUCK.toAabbs().toArray(new AABB[0]); + + /** + * @reason Multiple getEntities calls + the streams here are bad. We can + * use the overall suck shape AABB for the getEntities call, and check the individual shapes in the predicate. + * The suck shape is static, so we can cache the overall AABB and the individual AABBs. + * @author Spottedleaf + */ + @Overwrite + public static List getItemsAtAndAbove(final Level level, final Hopper hopper) { + final VoxelShape suckShape = hopper.getSuckShape(); + if (suckShape == Hopper.SUCK) { // support custom mod shapes (????) + final double shiftX = hopper.getLevelX() - 0.5D; + final double shiftY = hopper.getLevelY() - 0.5D; + final double shiftZ = hopper.getLevelZ() - 0.5D; + return level.getEntitiesOfClass(ItemEntity.class, HOPPER_ITEM_SUCK_OVERALL.move(shiftX, shiftY, shiftZ), (final Entity entity) -> { + if (!entity.isAlive()) { // EntitySelector.ENTITY_STILL_ALIVE + return false; + } + + for (final AABB aabb : HOPPER_ITEM_SUCK_INDIVIDUAL) { + if (aabb.move(shiftX, shiftY, shiftZ).intersects(entity.getBoundingBox())) { + return true; + } + } + + return false; + }); + } else { + return getItemsAtAndAboveSlow(level, hopper, suckShape); + } + } + + @Unique + private static List getItemsAtAndAboveSlow(Level level, Hopper hopper, VoxelShape suckShape) { + final double shiftX = hopper.getLevelX() - 0.5D; + final double shiftY = hopper.getLevelY() - 0.5D; + final double shiftZ = hopper.getLevelZ() - 0.5D; + + suckShape = suckShape.move(shiftX, shiftY, shiftZ); + + final List individual = suckShape.toAabbs(); + + return level.getEntitiesOfClass(ItemEntity.class, suckShape.bounds(), (final Entity entity) -> { + if (!entity.isAlive()) { // EntitySelector.ENTITY_STILL_ALIVE + return false; + } + + for (final AABB aabb : individual) { + if (aabb.intersects(entity.getBoundingBox())) { + return true; + } + } + + return false; + }); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/keep_alive_client/ServerGamePacketListenerImplMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/keep_alive_client/ServerGamePacketListenerImplMixin.java new file mode 100644 index 0000000..a53c097 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/keep_alive_client/ServerGamePacketListenerImplMixin.java @@ -0,0 +1,35 @@ +package ca.spottedleaf.moonrise.mixin.keep_alive_client; + +import net.minecraft.network.TickablePacketListener; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ServerGamePacketListener; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.server.network.ServerPlayerConnection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ServerGamePacketListenerImpl.class) +public abstract class ServerGamePacketListenerImplMixin implements ServerPlayerConnection, TickablePacketListener, ServerGamePacketListener { + + /** + * @reason Testing the explosion patch resulted in me being kicked for keepalive timeout as netty was unable to + * push enough packets with intellij debugger being the primary bottleneck. It shouldn't be kicking SP players for this. + * @author Spottedleaf + */ + @Redirect( + method = "tick", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/network/ServerGamePacketListenerImpl;disconnect(Lnet/minecraft/network/chat/Component;)V" + ) + ) + private void refuseSPKick(final ServerGamePacketListenerImpl instance, final Component component) { + if (Component.translatable("disconnect.timeout").equals(component) && + instance.isSingleplayerOwner()) { + return; + } + + instance.disconnect(component); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/poi_lookup/AcquirePoiMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/poi_lookup/AcquirePoiMixin.java new file mode 100644 index 0000000..44a88db --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/poi_lookup/AcquirePoiMixin.java @@ -0,0 +1,44 @@ +package ca.spottedleaf.moonrise.mixin.poi_lookup; + +import ca.spottedleaf.moonrise.patches.poi_lookup.PoiAccess; +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.world.entity.ai.behavior.AcquirePoi; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Stream; + +@Mixin(AcquirePoi.class) +public abstract class AcquirePoiMixin { + + /** + * @reason Limit return count for POI lookup to the limit vanilla will apply + * @author Spottedleaf + */ + @Redirect( + method = "method_46885", + at = @At( + target = "Lnet/minecraft/world/entity/ai/village/poi/PoiManager;findAllClosestFirstWithType(Ljava/util/function/Predicate;Ljava/util/function/Predicate;Lnet/minecraft/core/BlockPos;ILnet/minecraft/world/entity/ai/village/poi/PoiManager$Occupancy;)Ljava/util/stream/Stream;", + value = "INVOKE", + ordinal = 0 + ) + ) + private static Stream, BlockPos>> aaa(PoiManager poiManager, Predicate> predicate, + Predicate predicate2, BlockPos blockPos, int i, + PoiManager.Occupancy occup) { + final List, BlockPos>> ret = new ArrayList<>(); + + PoiAccess.findNearestPoiPositions( + poiManager, predicate, predicate2, blockPos, i, Double.MAX_VALUE, occup, true, 5, ret + ); + + return ret.stream(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/poi_lookup/PoiManagerMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/poi_lookup/PoiManagerMixin.java new file mode 100644 index 0000000..27ed371 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/poi_lookup/PoiManagerMixin.java @@ -0,0 +1,159 @@ +package ca.spottedleaf.moonrise.mixin.poi_lookup; + +import ca.spottedleaf.moonrise.patches.poi_lookup.PoiAccess; +import com.mojang.datafixers.DataFixer; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.util.RandomSource; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.chunk.storage.SectionStorage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +@Mixin(PoiManager.class) +public abstract class PoiManagerMixin extends SectionStorage { + public PoiManagerMixin(Path path, Function> function, Function function2, + DataFixer dataFixer, DataFixTypes dataFixTypes, boolean bl, RegistryAccess registryAccess, + LevelHeightAccessor levelHeightAccessor) { + super(path, function, function2, dataFixer, dataFixTypes, bl, registryAccess, levelHeightAccessor); + } + + /** + * @reason Route to use PoiAccess + * @author Spottedleaf + */ + @Overwrite + public Optional find(Predicate> typePredicate, Predicate posPredicate, BlockPos pos, + int radius, PoiManager.Occupancy occupationStatus) { + // Diff from Paper: use load=true + BlockPos ret = PoiAccess.findAnyPoiPosition((PoiManager)(Object)this, typePredicate, posPredicate, pos, radius, occupationStatus, true); + return Optional.ofNullable(ret); + } + + /** + * @reason Route to use PoiAccess + * @author Spottedleaf + */ + @Overwrite + public Optional findClosest(Predicate> typePredicate, BlockPos pos, int radius, + PoiManager.Occupancy occupationStatus) { + // Diff from Paper: use load=true + BlockPos ret = PoiAccess.findClosestPoiDataPosition((PoiManager)(Object)this, typePredicate, null, pos, radius, radius * radius, occupationStatus, true); + return Optional.ofNullable(ret); + } + + /** + * @reason Route to use PoiAccess + * @author Spottedleaf + */ + @Overwrite + public Optional, BlockPos>> findClosestWithType(Predicate> typePredicate, BlockPos pos, + int radius, PoiManager.Occupancy occupationStatus) { + // Diff from Paper: use load=true + return Optional.ofNullable(PoiAccess.findClosestPoiDataTypeAndPosition( + (PoiManager)(Object)this, typePredicate, null, pos, radius, radius * radius, occupationStatus, true + )); + } + + /** + * @reason Route to use PoiAccess + * @author Spottedleaf + */ + @Overwrite + public Optional findClosest(Predicate> typePredicate, Predicate posPredicate, BlockPos pos, + int radius, PoiManager.Occupancy occupationStatus) { + // Diff from Paper: use load=true + BlockPos ret = PoiAccess.findClosestPoiDataPosition((PoiManager)(Object)this, typePredicate, posPredicate, pos, radius, radius * radius, occupationStatus, true); + return Optional.ofNullable(ret); + } + + /** + * @reason Route to use PoiAccess + * @author Spottedleaf + */ + @Overwrite + public Optional take(Predicate> typePredicate, BiPredicate, BlockPos> biPredicate, + BlockPos pos, int radius) { + // Diff from Paper: use load=true + final PoiRecord closest = PoiAccess.findClosestPoiDataRecord( + (PoiManager)(Object)this, typePredicate, biPredicate, pos, radius, radius * radius, PoiManager.Occupancy.HAS_SPACE, true + ); + return Optional.ofNullable(closest).map(poi -> { + poi.acquireTicket(); + return poi.getPos(); + }); + } + + /** + * @reason Route to use PoiAccess + * @author Spottedleaf + */ + @Overwrite + public Optional getRandom(Predicate> typePredicate, Predicate positionPredicate, + PoiManager.Occupancy occupationStatus, BlockPos pos, int radius, RandomSource random) { + List list = new ArrayList<>(); + // Diff from Paper: use load=true + PoiAccess.findAnyPoiRecords( + (PoiManager)(Object)this, typePredicate, positionPredicate, pos, radius, occupationStatus, true, Integer.MAX_VALUE, list + ); + + // the old method shuffled the list and then tried to find the first element in it that + // matched positionPredicate, however we moved positionPredicate into the poi search. This means we can avoid a + // shuffle entirely, and just pick a random element from list + if (list.isEmpty()) { + return Optional.empty(); + } + + return Optional.ofNullable(list.get(random.nextInt(list.size())).getPos()); + } + + /** + * @reason Route to use PoiAccess + * @author Spottedleaf + */ + @Overwrite + public Stream, BlockPos>> findAllWithType(Predicate> predicate, Predicate predicate2, + BlockPos blockPos, int i, PoiManager.Occupancy occupancy) { + List, BlockPos>> ret = new ArrayList<>(); + + PoiAccess.findAnyPoiPositions( + (PoiManager)(Object)this, predicate, predicate2, blockPos, i, occupancy, true, + Integer.MAX_VALUE, ret + ); + + return ret.stream(); + } + + /** + * @reason Route to use PoiAccess + * @author Spottedleaf + */ + @Overwrite + public Stream, BlockPos>> findAllClosestFirstWithType(Predicate> predicate, + Predicate predicate2, BlockPos blockPos, + int i, PoiManager.Occupancy occupancy) { + List, BlockPos>> ret = new ArrayList<>(); + + PoiAccess.findNearestPoiPositions( + (PoiManager)(Object)this, predicate, predicate2, blockPos, i, Double.MAX_VALUE, occupancy, true, Integer.MAX_VALUE, ret + ); + + return ret.stream(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/poi_lookup/PortalForcerMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/poi_lookup/PortalForcerMixin.java new file mode 100644 index 0000000..0fcef6f --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/poi_lookup/PortalForcerMixin.java @@ -0,0 +1,84 @@ +package ca.spottedleaf.moonrise.mixin.poi_lookup; + +import ca.spottedleaf.moonrise.patches.poi_lookup.PoiAccess; +import net.minecraft.BlockUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.entity.ai.village.poi.PoiTypes; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.levelgen.BelowZeroRetrogen; +import net.minecraft.world.level.portal.PortalForcer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Mixin(PortalForcer.class) +public abstract class PortalForcerMixin { + + @Shadow + @Final + private ServerLevel level; + + /** + * @reason Route to use PoiAccess + * @author Spottedleaf + */ + @Overwrite + public Optional findPortalAround(BlockPos blockPos, boolean bl, WorldBorder worldBorder) { + PoiManager poiManager = this.level.getPoiManager(); + int i = bl ? 16 : 128; + List records = new ArrayList<>(); + PoiAccess.findClosestPoiDataRecords( + poiManager, type -> type.is(PoiTypes.NETHER_PORTAL), + (BlockPos pos) -> { + ChunkAccess lowest = this.level.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.EMPTY); + BelowZeroRetrogen belowZeroRetrogen; + if (!lowest.getStatus().isOrAfter(ChunkStatus.FULL) + // check below zero retrogen so that pre 1.17 worlds still load portals (JMP) + && ((belowZeroRetrogen = lowest.getBelowZeroRetrogen()) == null || !belowZeroRetrogen.targetStatus().isOrAfter(ChunkStatus.SPAWN))) { + // why would we generate the chunk? + return false; + } + if (!worldBorder.isWithinBounds(pos)) { + return false; + } + return lowest.getBlockState(pos).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); + }, + blockPos, i, Double.MAX_VALUE, PoiManager.Occupancy.ANY, true, records + ); + + // this gets us most of the way there, but Vanilla biases lower y values. + PoiRecord lowestYRecord = null; + for (PoiRecord record : records) { + if (lowestYRecord == null) { + lowestYRecord = record; + } else if (lowestYRecord.getPos().getY() > record.getPos().getY()) { + lowestYRecord = record; + } + } + // now we're done + if (lowestYRecord == null) { + return Optional.empty(); + } + + BlockPos blockPos1 = lowestYRecord.getPos(); + this.level.getChunkSource().addRegionTicket(TicketType.PORTAL, new ChunkPos(blockPos1), 3, blockPos1); + BlockState blockState = this.level.getBlockState(blockPos1); + return Optional.of(BlockUtil.getLargestRectangleAround(blockPos1, blockState.getValue(BlockStateProperties.HORIZONTAL_AXIS), 21, Direction.Axis.Y, 21, (blockPosx) -> { + return this.level.getBlockState(blockPosx) == blockState; + })); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/profiler/MinecraftMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/profiler/MinecraftMixin.java new file mode 100644 index 0000000..57e50ca --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/profiler/MinecraftMixin.java @@ -0,0 +1,44 @@ +package ca.spottedleaf.moonrise.mixin.profiler; + +import ca.spottedleaf.leafprofiler.client.ClientProfilerInstance; +import com.mojang.blaze3d.platform.WindowEventHandler; +import net.minecraft.client.Minecraft; +import net.minecraft.util.profiling.ContinuousProfiler; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.util.profiling.SingleTickProfiler; +import net.minecraft.util.thread.ReentrantBlockableEventLoop; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(Minecraft.class) +public abstract class MinecraftMixin extends ReentrantBlockableEventLoop implements WindowEventHandler { + + @Shadow + @Final + private ContinuousProfiler fpsPieProfiler; + + public MinecraftMixin(String string) { + super(string); + } + + @Unique + private final ClientProfilerInstance leafProfiler = new ClientProfilerInstance(); + + /** + * @reason Use our own profiler for client + * @author Spottedleaf + */ + @Overwrite + private ProfilerFiller constructProfiler(final boolean shouldRenderFPSPie, final SingleTickProfiler singleTickProfiler) { + if (shouldRenderFPSPie) { + this.fpsPieProfiler.enable(); + } else { + this.fpsPieProfiler.disable(); + } + + return this.leafProfiler; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/blockstate/BlockStateBaseMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/blockstate/BlockStateBaseMixin.java new file mode 100644 index 0000000..43d6876 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/blockstate/BlockStateBaseMixin.java @@ -0,0 +1,64 @@ +package ca.spottedleaf.moonrise.mixin.starlight.blockstate; + +import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState; +import com.google.common.collect.ImmutableMap; +import com.mojang.serialization.MapCodec; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateHolder; +import net.minecraft.world.level.block.state.properties.Property; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(BlockBehaviour.BlockStateBase.class) +public abstract class BlockStateBaseMixin extends StateHolder implements StarlightAbstractBlockState { + + @Shadow + @Final + private boolean useShapeForLightOcclusion; + + @Shadow + @Final + private boolean canOcclude; + + @Shadow + protected BlockBehaviour.BlockStateBase.Cache cache; + + @Unique + private int opacityIfCached; + + @Unique + private boolean isConditionallyFullOpaque; + + protected BlockStateBaseMixin(final Block object, final ImmutableMap, Comparable> immutableMap, final MapCodec mapCodec) { + super(object, immutableMap, mapCodec); + } + + /** + * Initialises our light state for this block. + */ + @Inject( + method = "initCache", + at = @At("RETURN") + ) + public void initLightAccessState(final CallbackInfo ci) { + this.isConditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion; + this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque ? -1 : this.cache.lightBlock; + } + + @Override + public final boolean isConditionallyFullOpaque() { + return this.isConditionallyFullOpaque; + } + + @Override + public final int getOpacityIfCached() { + return this.opacityIfCached; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/chunk/ChunkAccessMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/chunk/ChunkAccessMixin.java new file mode 100644 index 0000000..b8d3317 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/chunk/ChunkAccessMixin.java @@ -0,0 +1,63 @@ +package ca.spottedleaf.moonrise.mixin.starlight.chunk; + +import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; +import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray; +import net.minecraft.world.level.chunk.ChunkAccess; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(ChunkAccess.class) +public abstract class ChunkAccessMixin implements StarlightChunk { + + @Unique + private volatile SWMRNibbleArray[] blockNibbles; + + @Unique + private volatile SWMRNibbleArray[] skyNibbles; + + @Unique + private volatile boolean[] skyEmptinessMap; + + @Unique + private volatile boolean[] blockEmptinessMap; + + @Override + public SWMRNibbleArray[] getBlockNibbles() { + return this.blockNibbles; + } + + @Override + public void setBlockNibbles(final SWMRNibbleArray[] nibbles) { + this.blockNibbles = nibbles; + } + + @Override + public SWMRNibbleArray[] getSkyNibbles() { + return this.skyNibbles; + } + + @Override + public void setSkyNibbles(final SWMRNibbleArray[] nibbles) { + this.skyNibbles = nibbles; + } + + @Override + public boolean[] getSkyEmptinessMap() { + return this.skyEmptinessMap; + } + + @Override + public void setSkyEmptinessMap(final boolean[] emptinessMap) { + this.skyEmptinessMap = emptinessMap; + } + + @Override + public boolean[] getBlockEmptinessMap() { + return this.blockEmptinessMap; + } + + @Override + public void setBlockEmptinessMap(final boolean[] emptinessMap) { + this.blockEmptinessMap = emptinessMap; + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/chunk/EmptyLevelChunkMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/chunk/EmptyLevelChunkMixin.java new file mode 100644 index 0000000..14be3ae --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/chunk/EmptyLevelChunkMixin.java @@ -0,0 +1,50 @@ +package ca.spottedleaf.moonrise.mixin.starlight.chunk; + +import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; +import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.EmptyLevelChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(EmptyLevelChunk.class) +public abstract class EmptyLevelChunkMixin extends LevelChunk implements StarlightChunk { + + public EmptyLevelChunkMixin(final Level level, final ChunkPos pos) { + super(level, pos); + } + + @Override + public SWMRNibbleArray[] getBlockNibbles() { + return StarLightEngine.getFilledEmptyLight(this.getLevel()); + } + + @Override + public void setBlockNibbles(final SWMRNibbleArray[] nibbles) {} + + @Override + public SWMRNibbleArray[] getSkyNibbles() { + return StarLightEngine.getFilledEmptyLight(this.getLevel()); + } + + @Override + public void setSkyNibbles(final SWMRNibbleArray[] nibbles) {} + + @Override + public boolean[] getSkyEmptinessMap() { + return null; + } + + @Override + public void setSkyEmptinessMap(final boolean[] emptinessMap) {} + + @Override + public boolean[] getBlockEmptinessMap() { + return null; + } + + @Override + public void setBlockEmptinessMap(final boolean[] emptinessMap) {} +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/chunk/ImposterProtoChunkMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/chunk/ImposterProtoChunkMixin.java new file mode 100644 index 0000000..bbb2848 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/chunk/ImposterProtoChunkMixin.java @@ -0,0 +1,64 @@ +package ca.spottedleaf.moonrise.mixin.starlight.chunk; + +import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; +import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.chunk.ImposterProtoChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.UpgradeData; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(ImposterProtoChunk.class) +public abstract class ImposterProtoChunkMixin extends ProtoChunk implements StarlightChunk { + + @Final + @Shadow + private LevelChunk wrapped; + + public ImposterProtoChunkMixin(final LevelChunk levelChunk, final boolean bl) { + super(levelChunk.getPos(), UpgradeData.EMPTY, levelChunk, levelChunk.getLevel().registryAccess().registryOrThrow(Registries.BIOME), levelChunk.getBlendingData()); + } + + @Override + public SWMRNibbleArray[] getBlockNibbles() { + return ((StarlightChunk)this.wrapped).getBlockNibbles(); + } + + @Override + public void setBlockNibbles(final SWMRNibbleArray[] nibbles) { + ((StarlightChunk)this.wrapped).setBlockNibbles(nibbles); + } + + @Override + public SWMRNibbleArray[] getSkyNibbles() { + return ((StarlightChunk)this.wrapped).getSkyNibbles(); + } + + @Override + public void setSkyNibbles(final SWMRNibbleArray[] nibbles) { + ((StarlightChunk)this.wrapped).setSkyNibbles(nibbles); + } + + @Override + public boolean[] getSkyEmptinessMap() { + return ((StarlightChunk)this.wrapped).getSkyEmptinessMap(); + } + + @Override + public void setSkyEmptinessMap(final boolean[] emptinessMap) { + ((StarlightChunk)this.wrapped).setSkyEmptinessMap(emptinessMap); + } + + @Override + public boolean[] getBlockEmptinessMap() { + return ((StarlightChunk)this.wrapped).getBlockEmptinessMap(); + } + + @Override + public void setBlockEmptinessMap(final boolean[] emptinessMap) { + ((StarlightChunk)this.wrapped).setBlockEmptinessMap(emptinessMap); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/chunk/LevelChunkMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/chunk/LevelChunkMixin.java new file mode 100644 index 0000000..5e1597e --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/chunk/LevelChunkMixin.java @@ -0,0 +1,49 @@ +package ca.spottedleaf.moonrise.mixin.starlight.chunk; + +import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import net.minecraft.world.ticks.LevelChunkTicks; +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; + +@Mixin(LevelChunk.class) +public abstract class LevelChunkMixin implements StarlightChunk { + + /** + * Copies the nibble data from the protochunk. + * TODO since this is a constructor inject, check for new constructors on update. + */ + @Inject( + method = "(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/chunk/ProtoChunk;Lnet/minecraft/world/level/chunk/LevelChunk$PostLoadProcessor;)V", + at = @At("TAIL") + ) + public void onTransitionToFull(ServerLevel serverLevel, ProtoChunk protoChunk, LevelChunk.PostLoadProcessor postLoadProcessor, CallbackInfo ci) { + this.setBlockNibbles(((StarlightChunk)protoChunk).getBlockNibbles()); + this.setSkyNibbles(((StarlightChunk)protoChunk).getSkyNibbles()); + this.setSkyEmptinessMap(((StarlightChunk)protoChunk).getSkyEmptinessMap()); + this.setBlockEmptinessMap(((StarlightChunk)protoChunk).getBlockEmptinessMap()); + } + + /** + * Initialises the nibble arrays to default values. + * TODO since this is a constructor inject, check for new constructors on update. + */ + @Inject( + method = "(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/chunk/UpgradeData;Lnet/minecraft/world/ticks/LevelChunkTicks;Lnet/minecraft/world/ticks/LevelChunkTicks;J[Lnet/minecraft/world/level/chunk/LevelChunkSection;Lnet/minecraft/world/level/chunk/LevelChunk$PostLoadProcessor;Lnet/minecraft/world/level/levelgen/blending/BlendingData;)V", + at = @At("TAIL") + ) + public void onConstruct(Level level, ChunkPos chunkPos, UpgradeData upgradeData, LevelChunkTicks levelChunkTicks, LevelChunkTicks levelChunkTicks2, long l, LevelChunkSection[] levelChunkSections, LevelChunk.PostLoadProcessor postLoadProcessor, BlendingData blendingData, CallbackInfo ci) { + this.setBlockNibbles(StarLightEngine.getFilledEmptyLight(level)); + this.setSkyNibbles(StarLightEngine.getFilledEmptyLight(level)); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/chunk/ProtoChunkMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/chunk/ProtoChunkMixin.java new file mode 100644 index 0000000..9126b62 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/chunk/ProtoChunkMixin.java @@ -0,0 +1,37 @@ +package ca.spottedleaf.moonrise.mixin.starlight.chunk; + +import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine; +import net.minecraft.core.Registry; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.chunk.ImposterProtoChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import net.minecraft.world.ticks.ProtoChunkTicks; +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; + +@Mixin(ProtoChunk.class) +public abstract class ProtoChunkMixin implements StarlightChunk { + + /** + * Initialises the nibble arrays to default values. + * TODO since this is a constructor inject, check for new constructors on update. + */ + @Inject( + method = "(Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/chunk/UpgradeData;[Lnet/minecraft/world/level/chunk/LevelChunkSection;Lnet/minecraft/world/ticks/ProtoChunkTicks;Lnet/minecraft/world/ticks/ProtoChunkTicks;Lnet/minecraft/world/level/LevelHeightAccessor;Lnet/minecraft/core/Registry;Lnet/minecraft/world/level/levelgen/blending/BlendingData;)V", + at = @At("TAIL") + ) + public void onConstruct(ChunkPos chunkPos, UpgradeData upgradeData, LevelChunkSection[] levelChunkSections, ProtoChunkTicks protoChunkTicks, ProtoChunkTicks protoChunkTicks2, LevelHeightAccessor levelHeightAccessor, Registry registry, BlendingData blendingData, CallbackInfo ci) { + if ((Object)this instanceof ImposterProtoChunk) { + return; + } + this.setBlockNibbles(StarLightEngine.getFilledEmptyLight(levelHeightAccessor)); + this.setSkyNibbles(StarLightEngine.getFilledEmptyLight(levelHeightAccessor)); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/lightengine/LevelLightEngineMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/lightengine/LevelLightEngineMixin.java new file mode 100644 index 0000000..07f1dbe --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/lightengine/LevelLightEngineMixin.java @@ -0,0 +1,255 @@ +package ca.spottedleaf.moonrise.mixin.starlight.lightengine; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; +import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.DataLayer; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.level.lighting.LayerLightEventListener; +import net.minecraft.world.level.lighting.LevelLightEngine; +import net.minecraft.world.level.lighting.LightEngine; +import net.minecraft.world.level.lighting.LightEventListener; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LevelLightEngine.class) +public abstract class LevelLightEngineMixin implements LightEventListener, StarLightLightingProvider { + + @Shadow + @Nullable + private LightEngine blockEngine; + + @Shadow + @Nullable + private LightEngine skyEngine; + + @Unique + protected StarLightInterface lightEngine; + + @Override + public final StarLightInterface getLightEngine() { + return this.lightEngine; + } + + /** + * + * TODO since this is a constructor inject, check on update for new constructors + */ + @Inject( + method = "", at = @At("TAIL") + ) + public void construct(final LightChunkGetter chunkProvider, final boolean hasBlockLight, final boolean hasSkyLight, + final CallbackInfo ci) { + // avoid ClassCastException in cases where custom LightChunkGetters do not return a Level from getLevel() + if (chunkProvider.getLevel() instanceof Level) { + this.lightEngine = new StarLightInterface(chunkProvider, hasSkyLight, hasBlockLight, (LevelLightEngine)(Object)this); + } else { + this.lightEngine = new StarLightInterface(null, hasSkyLight, hasBlockLight, (LevelLightEngine)(Object)this); + } + // intentionally destroy mods hooking into old light engine state + this.blockEngine = null; + this.skyEngine = null; + } + + /** + * @reason Route to new light engine + * @author Spottedleaf + */ + @Overwrite + public void checkBlock(final BlockPos pos) { + this.lightEngine.blockChange(pos.immutable()); + } + + /** + * @reason Route to new light engine + * @author Spottedleaf + */ + @Overwrite + public boolean hasLightWork() { + // route to new light engine + return this.lightEngine.hasUpdates(); + } + + /** + * @reason Hook into new light engine for light updates + * @author Spottedleaf + */ + @Overwrite + public int runLightUpdates() { + // replace impl + final boolean hadUpdates = this.hasLightWork(); + this.lightEngine.propagateChanges(); + return hadUpdates ? 1 : 0; + } + + /** + * @reason New light engine hook for handling empty section changes + * @author Spottedleaf + */ + @Overwrite + public void updateSectionStatus(final SectionPos pos, final boolean notReady) { + this.lightEngine.sectionChange(pos, notReady); + } + + /** + * @reason Avoid messing with the vanilla light engine state + * @author Spottedleaf + */ + @Overwrite + public void setLightEnabled(final ChunkPos pos, final boolean lightEnabled) { + // not invoked by the client + } + + /** + * @reason Avoid messing with the vanilla light engine state + * @author Spottedleaf + */ + @Overwrite + public void propagateLightSources(ChunkPos param0) { + // not invoked by the client + } + + /** + * @reason Replace light views with our own that hook into the new light engine instead of vanilla's + * @author Spottedleaf + */ + @Overwrite + public LayerLightEventListener getLayerListener(final LightLayer lightType) { + return lightType == LightLayer.BLOCK ? this.lightEngine.getBlockReader() : this.lightEngine.getSkyReader(); + } + + /** + * @reason Avoid messing with the vanilla light engine state + * @author Spottedleaf + */ + @Overwrite + public void queueSectionData(final LightLayer lightType, final SectionPos pos, @Nullable final DataLayer nibble) { + // do not allow modification of data from the non-chunk load hooks + } + + /** + * @reason Avoid messing with the vanilla light engine state + * @author Spottedleaf + */ + @Overwrite + public String getDebugData(final LightLayer lightType, final SectionPos pos) { + // TODO would be nice to make use of this + return "n/a"; + } + + /** + * @reason Avoid messing with the vanilla light engine state + * @author Spottedleaf + */ + @Overwrite + public void retainData(final ChunkPos pos, final boolean retainData) { + // not used by new light impl + } + + /** + * @reason Need to use our own hooks for retrieving light data + * @author Spottedleaf + */ + @Overwrite + public int getRawBrightness(final BlockPos pos, final int ambientDarkness) { + // need to use new light hooks for this + return this.lightEngine.getRawBrightness(pos, ambientDarkness); + } + + /** + * @reason Need to use our own hooks for checking this state + * @author Spottedleaf + */ + @Overwrite + public boolean lightOnInSection(final SectionPos pos) { + final long key = CoordinateUtils.getChunkKey(pos.getX(), pos.getZ()); + return (!this.lightEngine.hasBlockLight() || this.blockLightMap.get(key) != null) && (!this.lightEngine.hasSkyLight() || this.skyLightMap.get(key) != null); + } + + @Unique + protected final Long2ObjectOpenHashMap blockLightMap = new Long2ObjectOpenHashMap<>(); + + @Unique + protected final Long2ObjectOpenHashMap skyLightMap = new Long2ObjectOpenHashMap<>(); + + @Override + public void clientUpdateLight(final LightLayer lightType, final SectionPos pos, + final DataLayer nibble, final boolean trustEdges) { + if (((Object)this).getClass() != LevelLightEngine.class) { + throw new IllegalStateException("This hook is for the CLIENT ONLY"); + } + // data storage changed with new light impl + final ChunkAccess chunk = this.getLightEngine().getAnyChunkNow(pos.getX(), pos.getZ()); + switch (lightType) { + case BLOCK: { + final SWMRNibbleArray[] blockNibbles = this.blockLightMap.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { + return StarLightEngine.getFilledEmptyLight(this.lightEngine.getWorld()); + }); + + blockNibbles[pos.getY() - WorldUtil.getMinLightSection(this.lightEngine.getWorld())] = SWMRNibbleArray.fromVanilla(nibble); + + if (chunk != null) { + ((StarlightChunk)chunk).setBlockNibbles(blockNibbles); + this.lightEngine.getLightAccess().onLightUpdate(LightLayer.BLOCK, pos); + } + break; + } + case SKY: { + final SWMRNibbleArray[] skyNibbles = this.skyLightMap.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { + return StarLightEngine.getFilledEmptyLight(this.lightEngine.getWorld()); + }); + + skyNibbles[pos.getY() - WorldUtil.getMinLightSection(this.lightEngine.getWorld())] = SWMRNibbleArray.fromVanilla(nibble); + + if (chunk != null) { + ((StarlightChunk)chunk).setSkyNibbles(skyNibbles); + this.lightEngine.getLightAccess().onLightUpdate(LightLayer.SKY, pos); + } + break; + } + } + } + + @Override + public void clientRemoveLightData(final ChunkPos chunkPos) { + if (((Object)this).getClass() != LevelLightEngine.class) { + throw new IllegalStateException("This hook is for the CLIENT ONLY"); + } + this.blockLightMap.remove(CoordinateUtils.getChunkKey(chunkPos)); + this.skyLightMap.remove(CoordinateUtils.getChunkKey(chunkPos)); + } + + @Override + public void clientChunkLoad(final ChunkPos pos, final LevelChunk chunk) { + if (((Object)this).getClass() != LevelLightEngine.class) { + throw new IllegalStateException("This hook is for the CLIENT ONLY"); + } + final long key = CoordinateUtils.getChunkKey(pos); + final SWMRNibbleArray[] blockNibbles = this.blockLightMap.get(key); + final SWMRNibbleArray[] skyNibbles = this.skyLightMap.get(key); + if (blockNibbles != null) { + ((StarlightChunk)chunk).setBlockNibbles(blockNibbles); + } + if (skyNibbles != null) { + ((StarlightChunk)chunk).setSkyNibbles(skyNibbles); + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/lightengine/ThreadedLevelLightEngineMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/lightengine/ThreadedLevelLightEngineMixin.java new file mode 100644 index 0000000..6fb699c --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/lightengine/ThreadedLevelLightEngineMixin.java @@ -0,0 +1,227 @@ +package ca.spottedleaf.moonrise.mixin.starlight.lightengine; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ThreadedLevelLightEngine; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.DataLayer; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.level.lighting.LevelLightEngine; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +@Mixin(ThreadedLevelLightEngine.class) +public abstract class ThreadedLevelLightEngineMixin extends LevelLightEngine implements StarLightLightingProvider { + + @Final + @Shadow + private ChunkMap chunkMap; + + @Final + @Shadow + private static Logger LOGGER; + + @Shadow + public abstract void tryScheduleUpdate(); + + public ThreadedLevelLightEngineMixin(final LightChunkGetter chunkProvider, final boolean hasBlockLight, final boolean hasSkyLight) { + super(chunkProvider, hasBlockLight, hasSkyLight); + } + + @Unique + private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); + + @Unique + private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, + final Supplier runnable) { + final ServerLevel world = (ServerLevel)this.getLightEngine().getWorld(); + + final ChunkAccess center = this.getLightEngine().getAnyChunkNow(chunkX, chunkZ); + if (center == null || !center.getStatus().isOrAfter(ChunkStatus.LIGHT)) { + // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing + // chunk scheduling, we could be lighting and generating a chunk at the same time + return; + } + + if (center.getStatus() != ChunkStatus.FULL) { + // do not keep chunk loaded, we are probably in a gen thread + // if we proceed to add a ticket the chunk will be loaded, which is not what we want (avoid cascading gen) + runnable.get(); + return; + } + + if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) { + // ticket logic is not safe to run off-main, re-schedule + world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> { + this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable); + }); + return; + } + + final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + final StarLightInterface.LightQueue.ChunkTasks updateFuture = runnable.get(); + + if (updateFuture == null) { + // not scheduled + return; + } + + if (updateFuture.isTicketAdded) { + // ticket already added + return; + } + updateFuture.isTicketAdded = true; + + final int references = this.chunksBeingWorkedOn.addTo(key, 1); + if (references == 0) { + final ChunkPos pos = new ChunkPos(chunkX, chunkZ); + world.getChunkSource().addRegionTicket(StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); + } + + updateFuture.onComplete.thenAcceptAsync((final Void ignore) -> { + final int newReferences = this.chunksBeingWorkedOn.get(key); + if (newReferences == 1) { + this.chunksBeingWorkedOn.remove(key); + final ChunkPos pos = new ChunkPos(chunkX, chunkZ); + world.getChunkSource().removeRegionTicket(StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); + } else { + this.chunksBeingWorkedOn.put(key, newReferences - 1); + } + }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> { + if (thr != null) { + LOGGER.error("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr); + } + }); + } + + /** + * @reason Redirect scheduling call away from the vanilla light engine, as well as enforce + * that chunk neighbours are loaded before the processing can occur + * @author Spottedleaf + */ + @Overwrite + public void checkBlock(final BlockPos pos) { + final BlockPos posCopy = pos.immutable(); + this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> { + return this.getLightEngine().blockChange(posCopy); + }); + } + + /** + * @reason Avoid messing with the vanilla light engine state + * @author Spottedleaf + */ + @Overwrite + public void updateChunkStatus(final ChunkPos pos) {} + + /** + * @reason Redirect to schedule for our own logic, as well as ensure 1 radius neighbours + * are loaded + * Note: Our scheduling logic will discard this call if the chunk is not lit, unloaded, or not at LIGHT stage yet. + * @author Spottedleaf + */ + @Overwrite + public void updateSectionStatus(final SectionPos pos, final boolean notReady) { + this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> { + return this.getLightEngine().sectionChange(pos, notReady); + }); + } + + /** + * @reason Avoid messing with the vanilla light engine state + * @author Spottedleaf + */ + @Overwrite + public void propagateLightSources(final ChunkPos pos) { + // handled by light() + } + + /** + * @reason Avoid messing with the vanilla light engine state + * @author Spottedleaf + */ + @Overwrite + public void setLightEnabled(final ChunkPos pos, final boolean lightEnabled) { + // light impl does not need to do this + } + + /** + * @reason Light data is now attached to chunks, and this means we need to hook into chunk loading logic + * to load the data rather than rely on this call. This call also would mess with the vanilla light engine state. + * @author Spottedleaf + */ + @Overwrite + public void queueSectionData(final LightLayer lightType, final SectionPos pos, final @Nullable DataLayer nibbles) { + // load hooks inside ChunkSerializer + } + + /** + * @reason Avoid messing with the vanilla light engine state + * @author Spottedleaf + */ + @Overwrite + public void retainData(final ChunkPos pos, final boolean retainData) { + // light impl does not need to do this + } + + /** + * @reason Starlight does not have to do this + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture initializeLight(final ChunkAccess chunk, final boolean lit) { + return CompletableFuture.completedFuture(chunk); + } + + /** + * @reason Route to new logic to either light or just load the data + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture lightChunk(final ChunkAccess chunk, final boolean lit) { + final ChunkPos chunkPos = chunk.getPos(); + + return CompletableFuture.supplyAsync(() -> { + final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk); + if (!lit) { + chunk.setLightCorrect(false); + this.getLightEngine().lightChunk(chunk, emptySections); + chunk.setLightCorrect(true); + } else { + this.getLightEngine().forceLoadInChunk(chunk, emptySections); + // can't really force the chunk to be edged checked, as we need neighbouring chunks - but we don't have + // them, so if it's not loaded then i guess we can't do edge checks. later loads of the chunk should + // catch what we miss here. + this.getLightEngine().checkChunkEdges(chunkPos.x, chunkPos.z); + } + + this.chunkMap.releaseLightTicket(chunkPos); + return chunk; + }, (runnable) -> { + this.getLightEngine().scheduleChunkLight(chunkPos, runnable); + this.tryScheduleUpdate(); + }).whenComplete((final ChunkAccess c, final Throwable throwable) -> { + if (throwable != null) { + LOGGER.error("Failed to light chunk " + chunkPos, throwable); + } + }); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/multiplayer/ClientPacketListenerMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/multiplayer/ClientPacketListenerMixin.java new file mode 100644 index 0000000..8df44d5 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/multiplayer/ClientPacketListenerMixin.java @@ -0,0 +1,136 @@ +package ca.spottedleaf.moonrise.mixin.starlight.multiplayer; + +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.core.SectionPos; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket; +import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; +import net.minecraft.network.protocol.game.ClientboundLightUpdatePacketData; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.DataLayer; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.lighting.LevelLightEngine; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = ClientPacketListener.class, priority = 1001) +public abstract class ClientPacketListenerMixin implements ClientGamePacketListener { + + /* + The call behaviors in the packet handler are much more clear about how they should affect the light engine, + and as a result makes the client light load/unload more reliable + */ + + @Shadow + private ClientLevel level; + + /* + Now in 1.18 Mojang has added logic to delay rendering chunks until their lighting is ready (as they are delaying + light updates). Fortunately for us, Starlight doesn't take any kind of hit loading in light data. So we have no reason + to delay the light updates at all (and we shouldn't delay them or else desync might occur - such as with block updates). + */ + + @Shadow + protected abstract void applyLightData(final int chunkX, final int chunkZ, final ClientboundLightUpdatePacketData clientboundLightUpdatePacketData); + + @Shadow + protected abstract void enableChunkLight(final LevelChunk levelChunk, final int chunkX, final int chunkZ); + + /** + * Call the runnable immediately to prevent desync + * @author Spottedleaf + */ + @Redirect( + method = "handleLightUpdatePacket", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/multiplayer/ClientLevel;queueLightUpdate(Ljava/lang/Runnable;)V" + ) + ) + private void starlightCallUpdateImmediately(final ClientLevel instance, final Runnable runnable) { + runnable.run(); + } + + /** + * Re-route light update packet to our own logic + * @author Spottedleaf + */ + @Redirect( + method = "readSectionList", + at = @At( + target = "Lnet/minecraft/world/level/lighting/LevelLightEngine;queueSectionData(Lnet/minecraft/world/level/LightLayer;Lnet/minecraft/core/SectionPos;Lnet/minecraft/world/level/chunk/DataLayer;)V", + value = "INVOKE", + ordinal = 0 + ) + ) + private void loadLightDataHook(final LevelLightEngine lightEngine, final LightLayer lightType, final SectionPos pos, + final @Nullable DataLayer nibble) { + ((StarLightLightingProvider)this.level.getChunkSource().getLightEngine()).clientUpdateLight(lightType, pos, nibble, true); + } + + + /** + * Avoid calling Vanilla's logic here, and instead call our own. + * @author Spottedleaf + */ + @Redirect( + method = "handleForgetLevelChunk", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/multiplayer/ClientPacketListener;queueLightRemoval(Lnet/minecraft/network/protocol/game/ClientboundForgetLevelChunkPacket;)V" + ) + ) + private void unloadLightDataHook(final ClientPacketListener instance, final ClientboundForgetLevelChunkPacket clientboundForgetLevelChunkPacket) { + ((StarLightLightingProvider)this.level.getChunkSource().getLightEngine()).clientRemoveLightData(new ChunkPos(clientboundForgetLevelChunkPacket.getX(), clientboundForgetLevelChunkPacket.getZ())); + } + + /** + * Don't call vanilla's load logic + */ + @Redirect( + method = "handleLevelChunkWithLight", + at = @At( + target = "Lnet/minecraft/client/multiplayer/ClientLevel;queueLightUpdate(Ljava/lang/Runnable;)V", + value = "INVOKE", + ordinal = 0 + ) + ) + private void postChunkLoadHookRedirect(final ClientLevel instance, final Runnable runnable) { + // don't call vanilla's logic, see below + } + + /** + * Hook for loading in a chunk to the world + * @author Spottedleaf + */ + @Inject( + method = "handleLevelChunkWithLight", + at = @At( + value = "RETURN" + ) + ) + private void postChunkLoadHook(final ClientboundLevelChunkWithLightPacket clientboundLevelChunkWithLightPacket, final CallbackInfo ci) { + final int chunkX = clientboundLevelChunkWithLightPacket.getX(); + final int chunkZ = clientboundLevelChunkWithLightPacket.getZ(); + final LevelChunk chunk = (LevelChunk)this.level.getChunk(chunkX, chunkZ, ChunkStatus.FULL, false); + if (chunk == null) { + // failed to load + return; + } + // load in light data from packet immediately + this.applyLightData(chunkX, chunkZ, clientboundLevelChunkWithLightPacket.getLightData()); + ((StarLightLightingProvider) this.level.getChunkSource().getLightEngine()).clientChunkLoad(new ChunkPos(chunkX, chunkZ), chunk); + + // we need this for the update chunk status call, so that it can tell starlight what sections are empty and such + this.enableChunkLight(chunk, chunkX, chunkZ); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/ChunkSerializerMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/ChunkSerializerMixin.java new file mode 100644 index 0000000..18efcad --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/ChunkSerializerMixin.java @@ -0,0 +1,43 @@ +package ca.spottedleaf.moonrise.mixin.starlight.world; + +import ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.storage.ChunkSerializer; +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.CallbackInfoReturnable; + +@Mixin(ChunkSerializer.class) +public abstract class ChunkSerializerMixin { + + /** + * Overwrites vanilla's light data with our own. + * TODO this needs to be checked on update to account for format changes + */ + @Inject( + method = "write", + at = @At("RETURN") + ) + private static void saveLightHook(final ServerLevel world, final ChunkAccess chunk, final CallbackInfoReturnable cir) { + SaveUtil.saveLightHook(world, chunk, cir.getReturnValue()); + } + + /** + * Loads our light data into the returned chunk object from the tag. + * TODO this needs to be checked on update to account for format changes + */ + @Inject( + method = "read", + at = @At("RETURN") + ) + private static void loadLightHook(final ServerLevel serverLevel, final PoiManager poiManager, final ChunkPos chunkPos, + final CompoundTag compoundTag, final CallbackInfoReturnable cir) { + SaveUtil.loadLightHook(serverLevel, chunkPos, compoundTag, cir.getReturnValue()); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/ClientLevelMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/ClientLevelMixin.java new file mode 100644 index 0000000..9ac1114 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/ClientLevelMixin.java @@ -0,0 +1,39 @@ +package ca.spottedleaf.moonrise.mixin.starlight.world; + +import ca.spottedleaf.moonrise.patches.starlight.world.StarlightWorld; +import net.minecraft.client.multiplayer.ClientChunkCache; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceKey; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.storage.WritableLevelData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.function.Supplier; + +@Mixin(ClientLevel.class) +public abstract class ClientLevelMixin extends Level implements StarlightWorld { + + protected ClientLevelMixin(WritableLevelData writableLevelData, ResourceKey resourceKey, RegistryAccess registryAccess, Holder holder, Supplier supplier, boolean bl, boolean bl2, long l, int i) { + super(writableLevelData, resourceKey, registryAccess, holder, supplier, bl, bl2, l, i); + } + + @Shadow + public abstract ClientChunkCache getChunkSource(); + + @Override + public final LevelChunk getChunkAtImmediately(final int chunkX, final int chunkZ) { + return this.getChunkSource().getChunk(chunkX, chunkZ, false); + } + + @Override + public final ChunkAccess getAnyChunkImmediately(int chunkX, int chunkZ) { + return this.getChunkSource().getChunk(chunkX, chunkZ, false); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/LevelMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/LevelMixin.java new file mode 100644 index 0000000..480b306 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/LevelMixin.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.mixin.starlight.world; + +import ca.spottedleaf.moonrise.patches.starlight.world.StarlightWorld; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(Level.class) +public abstract class LevelMixin implements LevelAccessor, AutoCloseable, StarlightWorld {} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/ServerWorldMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/ServerWorldMixin.java new file mode 100644 index 0000000..d4cf387 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/ServerWorldMixin.java @@ -0,0 +1,58 @@ +package ca.spottedleaf.moonrise.mixin.starlight.world; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.patches.starlight.world.StarlightWorld; +import com.mojang.datafixers.util.Either; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.storage.WritableLevelData; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import java.util.function.Supplier; + +@Mixin(ServerLevel.class) +public abstract class ServerWorldMixin extends Level implements WorldGenLevel, StarlightWorld { + + @Shadow + @Final + private ServerChunkCache chunkSource; + + protected ServerWorldMixin(WritableLevelData writableLevelData, ResourceKey resourceKey, RegistryAccess registryAccess, Holder holder, Supplier supplier, boolean bl, boolean bl2, long l, int i) { + super(writableLevelData, resourceKey, registryAccess, holder, supplier, bl, bl2, l, i); + } + + @Override + public final LevelChunk getChunkAtImmediately(final int chunkX, final int chunkZ) { + final ChunkMap storage = this.chunkSource.chunkMap; + final ChunkHolder holder = storage.getVisibleChunkIfPresent(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + if (holder == null) { + return null; + } + + final Either either = holder.getFutureIfPresentUnchecked(ChunkStatus.FULL).getNow(null); + + return either == null ? null : (LevelChunk)either.left().orElse(null); + } + + @Override + public final ChunkAccess getAnyChunkImmediately(final int chunkX, final int chunkZ) { + final ChunkMap storage = this.chunkSource.chunkMap; + final ChunkHolder holder = storage.getVisibleChunkIfPresent(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + return holder == null ? null : holder.getLastAvailable(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/util_thread_counts/UtilMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/util_thread_counts/UtilMixin.java new file mode 100644 index 0000000..5ad1f33 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/util_thread_counts/UtilMixin.java @@ -0,0 +1,36 @@ +package ca.spottedleaf.moonrise.mixin.util_thread_counts; + +import net.minecraft.Util; +import net.minecraft.util.Mth; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(Util.class) +public abstract class UtilMixin { + + /** + * @reason Don't over-allocate executor threads, they may choke the rest of the game + * @author Spottedleaf + */ + @Redirect( + method = "makeExecutor", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/util/Mth;clamp(III)I" + ) + ) + private static int correctThreadCounts(int value, final int min, final int max) { + final int cpus = Runtime.getRuntime().availableProcessors() / 2; + if (cpus <= 4) { + value = cpus <= 2 ? 1 : 2; + } else if (cpus <= 8) { + // [5, 8] + value = cpus <= 6 ? 3 : 4; + } else { + value = Math.min(8, cpus / 2); + } + + return Mth.clamp(value, min, max); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/util_time_source/UtilMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/util_time_source/UtilMixin.java new file mode 100644 index 0000000..25f8ea9 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/util_time_source/UtilMixin.java @@ -0,0 +1,21 @@ +package ca.spottedleaf.moonrise.mixin.util_time_source; + +import net.minecraft.Util; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +@Mixin(Util.class) +public abstract class UtilMixin { + + /** + * @reason GLFW clock will use the same one as nanoTime, except that it is JNI + * and cannot be inlined and incurs the JNI method invoke cost. This resolves + * the client profiler causing more lag than it should if there are many things + * being profiled such as many entities + * @author Spottedleaf + */ + @Overwrite + public static long getNanos() { + return System.nanoTime(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java new file mode 100644 index 0000000..28030b9 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.chunk_getblock; + +import net.minecraft.world.level.block.state.BlockState; + +public interface GetBlockChunk { + + public BlockState getBlock(final int x, final int y, final int z); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java new file mode 100644 index 0000000..ea44fe9 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java @@ -0,0 +1,1968 @@ +package ca.spottedleaf.moonrise.patches.collisions; + +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState; +import ca.spottedleaf.moonrise.patches.collisions.entity.CollisionEntity; +import ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData; +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape; +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; +import ca.spottedleaf.moonrise.patches.collisions.world.CollisionEntityGetter; +import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel; +import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevelChunkSection; +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.CollisionGetter; +import net.minecraft.world.level.EntityGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.ArrayVoxelShape; +import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape; +import net.minecraft.world.phys.shapes.BooleanOp; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.DiscreteVoxelShape; +import net.minecraft.world.phys.shapes.EntityCollisionContext; +import net.minecraft.world.phys.shapes.OffsetDoubleList; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +public final class CollisionUtil { + + public static final double COLLISION_EPSILON = 1.0E-7; + public static final DoubleArrayList ZERO_ONE = DoubleArrayList.wrap(new double[] { 0.0, 1.0 }); + + public static boolean isSpecialCollidingBlock(final net.minecraft.world.level.block.state.BlockBehaviour.BlockStateBase block) { + return block.hasLargeCollisionShape() || block.getBlock() == Blocks.MOVING_PISTON; + } + + public static boolean isEmpty(final AABB aabb) { + return (aabb.maxX - aabb.minX) < COLLISION_EPSILON || (aabb.maxY - aabb.minY) < COLLISION_EPSILON || (aabb.maxZ - aabb.minZ) < COLLISION_EPSILON; + } + + public static boolean isEmpty(final double minX, final double minY, final double minZ, + final double maxX, final double maxY, final double maxZ) { + return (maxX - minX) < COLLISION_EPSILON || (maxY - minY) < COLLISION_EPSILON || (maxZ - minZ) < COLLISION_EPSILON; + } + + public static AABB getBoxForChunk(final int chunkX, final int chunkZ) { + double x = (double)(chunkX << 4); + double z = (double)(chunkZ << 4); + // use a bounding box bigger than the chunk to prevent entities from entering it on move + return new AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON, + x + (16.0 + 3*COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*COLLISION_EPSILON)); + } + + /* + A couple of rules for VoxelShape collisions: + Two shapes only intersect if they are actually more than EPSILON units into each other. This also applies to movement + checks. + If the two shapes strictly collide, then the return value of a collide call will return a value in the opposite + direction of the source move. However, this value will not be greater in magnitude than EPSILON. Collision code + will automatically round it to 0. + */ + + public static boolean voxelShapeIntersect(final double minX1, final double minY1, final double minZ1, final double maxX1, + final double maxY1, final double maxZ1, final double minX2, final double minY2, + final double minZ2, final double maxX2, final double maxY2, final double maxZ2) { + return (minX1 - maxX2) < -COLLISION_EPSILON && (maxX1 - minX2) > COLLISION_EPSILON && + (minY1 - maxY2) < -COLLISION_EPSILON && (maxY1 - minY2) > COLLISION_EPSILON && + (minZ1 - maxZ2) < -COLLISION_EPSILON && (maxZ1 - minZ2) > COLLISION_EPSILON; + } + + public static boolean voxelShapeIntersect(final AABB box, final double minX, final double minY, final double minZ, + final double maxX, final double maxY, final double maxZ) { + return (box.minX - maxX) < -COLLISION_EPSILON && (box.maxX - minX) > COLLISION_EPSILON && + (box.minY - maxY) < -COLLISION_EPSILON && (box.maxY - minY) > COLLISION_EPSILON && + (box.minZ - maxZ) < -COLLISION_EPSILON && (box.maxZ - minZ) > COLLISION_EPSILON; + } + + public static boolean voxelShapeIntersect(final AABB box1, final AABB box2) { + return (box1.minX - box2.maxX) < -COLLISION_EPSILON && (box1.maxX - box2.minX) > COLLISION_EPSILON && + (box1.minY - box2.maxY) < -COLLISION_EPSILON && (box1.maxY - box2.minY) > COLLISION_EPSILON && + (box1.minZ - box2.maxZ) < -COLLISION_EPSILON && (box1.maxZ - box2.minZ) > COLLISION_EPSILON; + } + + // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON + public static double collideX(final AABB target, final AABB source, final double source_move) { + if ((source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON && + (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { + if (source_move >= 0.0) { + final double max_move = target.minX - source.maxX; // < 0.0 if no strict collision + if (max_move < -COLLISION_EPSILON) { + return source_move; + } + return Math.min(max_move, source_move); + } else { + final double max_move = target.maxX - source.minX; // > 0.0 if no strict collision + if (max_move > COLLISION_EPSILON) { + return source_move; + } + return Math.max(max_move, source_move); + } + } + return source_move; + } + + // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON + public static double collideY(final AABB target, final AABB source, final double source_move) { + if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && + (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { + if (source_move >= 0.0) { + final double max_move = target.minY - source.maxY; // < 0.0 if no strict collision + if (max_move < -COLLISION_EPSILON) { + return source_move; + } + return Math.min(max_move, source_move); + } else { + final double max_move = target.maxY - source.minY; // > 0.0 if no strict collision + if (max_move > COLLISION_EPSILON) { + return source_move; + } + return Math.max(max_move, source_move); + } + } + return source_move; + } + + // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON + public static double collideZ(final AABB target, final AABB source, final double source_move) { + if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && + (source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON) { + if (source_move >= 0.0) { + final double max_move = target.minZ - source.maxZ; // < 0.0 if no strict collision + if (max_move < -COLLISION_EPSILON) { + return source_move; + } + return Math.min(max_move, source_move); + } else { + final double max_move = target.maxZ - source.minZ; // > 0.0 if no strict collision + if (max_move > COLLISION_EPSILON) { + return source_move; + } + return Math.max(max_move, source_move); + } + } + return source_move; + } + + // startIndex and endIndex inclusive + // assumes indices are in range of array + private static int findFloor(final double[] values, final double value, int startIndex, int endIndex) { + do { + final int middle = (startIndex + endIndex) >>> 1; + final double middleVal = values[middle]; + + if (value < middleVal) { + endIndex = middle - 1; + } else { + startIndex = middle + 1; + } + } while (startIndex <= endIndex); + + return startIndex - 1; + } + + public static boolean voxelShapeIntersectNoEmpty(final VoxelShape voxel, final AABB aabb) { + if (voxel.isEmpty()) { + return false; + } + + // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true + + // offsets that should be applied to coords + final double off_x = ((CollisionVoxelShape)voxel).offsetX(); + final double off_y = ((CollisionVoxelShape)voxel).offsetY(); + final double off_z = ((CollisionVoxelShape)voxel).offsetZ(); + + final double[] coords_x = ((CollisionVoxelShape)voxel).rootCoordinatesX(); + final double[] coords_y = ((CollisionVoxelShape)voxel).rootCoordinatesY(); + final double[] coords_z = ((CollisionVoxelShape)voxel).rootCoordinatesZ(); + + final CachedShapeData cached_shape_data = ((CollisionVoxelShape)voxel).getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); + final int size_y = cached_shape_data.sizeY(); + final int size_z = cached_shape_data.sizeZ(); + + // note: voxel bitset with set index (x, y, z) indicates that + // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) + // is collidable. this is the fundamental principle of operation for the voxel collision operation + + // note: we should be offsetting coords, but we can also just subtract from source as well - which is + // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) + // note: for intersection, one we find the floor of the min we can use that as the start index + // for the next check as source max >= source min + // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, + // as this implies that coords[coords.length - 1] < source min + // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max + + final int floor_min_x = Math.max( + 0, + findFloor(coords_x, (aabb.minX - off_x) + COLLISION_EPSILON, 0, size_x) + ); + if (floor_min_x >= size_x) { + // cannot intersect + return false; + } + + final int ceil_max_x = Math.min( + size_x, + findFloor(coords_x, (aabb.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1 + ); + if (floor_min_x >= ceil_max_x) { + // cannot intersect + return false; + } + + final int floor_min_y = Math.max( + 0, + findFloor(coords_y, (aabb.minY - off_y) + COLLISION_EPSILON, 0, size_y) + ); + if (floor_min_y >= size_y) { + // cannot intersect + return false; + } + + final int ceil_max_y = Math.min( + size_y, + findFloor(coords_y, (aabb.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1 + ); + if (floor_min_y >= ceil_max_y) { + // cannot intersect + return false; + } + + final int floor_min_z = Math.max( + 0, + findFloor(coords_z, (aabb.minZ - off_z) + COLLISION_EPSILON, 0, size_z) + ); + if (floor_min_z >= size_z) { + // cannot intersect + return false; + } + + final int ceil_max_z = Math.min( + size_z, + findFloor(coords_z, (aabb.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1 + ); + if (floor_min_z >= ceil_max_z) { + // cannot intersect + return false; + } + + final long[] bitset = cached_shape_data.voxelSet(); + + // check bitset to check if any shapes in range are full + + final int mul_x = size_y*size_z; + for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { + for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { + for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { + final int index = curr_z + curr_y*size_z + curr_x*mul_x; + // note: JLS states long shift operators ANDS shift by 63 + if ((bitset[index >>> 6] & (1L << index)) != 0L) { + return true; + } + } + } + } + + return false; + } + + // assume !target.isEmpty() && abs(source_move) >= COLLISION_EPSILON + public static double collideX(final VoxelShape target, final AABB source, final double source_move) { + final AABB single_aabb = ((CollisionVoxelShape)target).getSingleAABBRepresentation(); + if (single_aabb != null) { + return collideX(single_aabb, source, source_move); + } + // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true + + // offsets that should be applied to coords + final double off_x = ((CollisionVoxelShape)target).offsetX(); + final double off_y = ((CollisionVoxelShape)target).offsetY(); + final double off_z = ((CollisionVoxelShape)target).offsetZ(); + + final double[] coords_x = ((CollisionVoxelShape)target).rootCoordinatesX(); + final double[] coords_y = ((CollisionVoxelShape)target).rootCoordinatesY(); + final double[] coords_z = ((CollisionVoxelShape)target).rootCoordinatesZ(); + + final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); + final int size_y = cached_shape_data.sizeY(); + final int size_z = cached_shape_data.sizeZ(); + + // note: voxel bitset with set index (x, y, z) indicates that + // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) + // is collidable. this is the fundamental principle of operation for the voxel collision operation + + + // note: we should be offsetting coords, but we can also just subtract from source as well - which is + // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) + // note: for intersection, one we find the floor of the min we can use that as the start index + // for the next check as source max >= source min + // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, + // as this implies that coords[coords.length - 1] < source min + // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max + + final int floor_min_y = Math.max( + 0, + findFloor(coords_y, (source.minY - off_y) + COLLISION_EPSILON, 0, size_y) + ); + if (floor_min_y >= size_y) { + // cannot intersect + return source_move; + } + + final int ceil_max_y = Math.min( + size_y, + findFloor(coords_y, (source.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1 + ); + if (floor_min_y >= ceil_max_y) { + // cannot intersect + return source_move; + } + + final int floor_min_z = Math.max( + 0, + findFloor(coords_z, (source.minZ - off_z) + COLLISION_EPSILON, 0, size_z) + ); + if (floor_min_z >= size_z) { + // cannot intersect + return source_move; + } + + final int ceil_max_z = Math.min( + size_z, + findFloor(coords_z, (source.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1 + ); + if (floor_min_z >= ceil_max_z) { + // cannot intersect + return source_move; + } + + // index = z + y*size_z + x*(size_z*size_y) + + final long[] bitset = cached_shape_data.voxelSet(); + + if (source_move > 0.0) { + final double source_max = source.maxX - off_x; + final int ceil_max_x = findFloor( + coords_x, source_max - COLLISION_EPSILON, 0, size_x + ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max + + // note: only the order of the first loop matters + + // note: we cannot collide with the face at index size on the collision axis for forward movement + + final int mul_x = size_y*size_z; + for (int curr_x = ceil_max_x; curr_x < size_x; ++curr_x) { + double max_dist = coords_x[curr_x] - source_max; + if (max_dist >= source_move) { + // if we reach here, then we will never have a case where + // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1] + // thus, we can return immediately + + // this optimization is important since this loop is bounded by size, and _not_ by + // a calculated max index based off of source_move - so it would be possible to check + // the whole intersected shape for collisions when we didn't need to! + return source_move; + } + if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON + max_dist = Math.min(max_dist, source_move); + } + for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { + for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { + final int index = curr_z + curr_y*size_z + curr_x*mul_x; + // note: JLS states long shift operators ANDS shift by 63 + if ((bitset[index >>> 6] & (1L << index)) != 0L) { + return max_dist; + } + } + } + } + + return source_move; + } else { + final double source_min = source.minX - off_x; + final int floor_min_x = findFloor( + coords_x, source_min + COLLISION_EPSILON, 0, size_x + ); + + // note: only the order of the first loop matters + + // note: we cannot collide with the face at index 0 on the collision axis for backwards movement + + // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the + // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1] + // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid + final int mul_x = size_y*size_z; + for (int curr_x = floor_min_x - 1; curr_x >= 0; --curr_x) { + double max_dist = coords_x[curr_x + 1] - source_min; + if (max_dist <= source_move) { + // if we reach here, then we will never have a case where + // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1] + // thus, we can return immediately + + // this optimization is important since this loop is possibly bounded by size, and _not_ by + // a calculated max index based off of source_move - so it would be possible to check + // the whole intersected shape for collisions when we didn't need to! + return source_move; + } + if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON + max_dist = Math.max(max_dist, source_move); + } + for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { + for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { + final int index = curr_z + curr_y*size_z + curr_x*mul_x; + // note: JLS states long shift operators ANDS shift by 63 + if ((bitset[index >>> 6] & (1L << index)) != 0L) { + return max_dist; + } + } + } + } + + return source_move; + } + } + + public static double collideY(final VoxelShape target, final AABB source, final double source_move) { + final AABB single_aabb = ((CollisionVoxelShape)target).getSingleAABBRepresentation(); + if (single_aabb != null) { + return collideY(single_aabb, source, source_move); + } + // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true + + // offsets that should be applied to coords + final double off_x = ((CollisionVoxelShape)target).offsetX(); + final double off_y = ((CollisionVoxelShape)target).offsetY(); + final double off_z = ((CollisionVoxelShape)target).offsetZ(); + + final double[] coords_x = ((CollisionVoxelShape)target).rootCoordinatesX(); + final double[] coords_y = ((CollisionVoxelShape)target).rootCoordinatesY(); + final double[] coords_z = ((CollisionVoxelShape)target).rootCoordinatesZ(); + + final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); + final int size_y = cached_shape_data.sizeY(); + final int size_z = cached_shape_data.sizeZ(); + + // note: voxel bitset with set index (x, y, z) indicates that + // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) + // is collidable. this is the fundamental principle of operation for the voxel collision operation + + + // note: we should be offsetting coords, but we can also just subtract from source as well - which is + // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) + // note: for intersection, one we find the floor of the min we can use that as the start index + // for the next check as source max >= source min + // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, + // as this implies that coords[coords.length - 1] < source min + // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max + + final int floor_min_x = Math.max( + 0, + findFloor(coords_x, (source.minX - off_x) + COLLISION_EPSILON, 0, size_x) + ); + if (floor_min_x >= size_x) { + // cannot intersect + return source_move; + } + + final int ceil_max_x = Math.min( + size_x, + findFloor(coords_x, (source.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1 + ); + if (floor_min_x >= ceil_max_x) { + // cannot intersect + return source_move; + } + + final int floor_min_z = Math.max( + 0, + findFloor(coords_z, (source.minZ - off_z) + COLLISION_EPSILON, 0, size_z) + ); + if (floor_min_z >= size_z) { + // cannot intersect + return source_move; + } + + final int ceil_max_z = Math.min( + size_z, + findFloor(coords_z, (source.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1 + ); + if (floor_min_z >= ceil_max_z) { + // cannot intersect + return source_move; + } + + // index = z + y*size_z + x*(size_z*size_y) + + final long[] bitset = cached_shape_data.voxelSet(); + + if (source_move > 0.0) { + final double source_max = source.maxY - off_y; + final int ceil_max_y = findFloor( + coords_y, source_max - COLLISION_EPSILON, 0, size_y + ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max + + // note: only the order of the first loop matters + + // note: we cannot collide with the face at index size on the collision axis for forward movement + + final int mul_x = size_y*size_z; + for (int curr_y = ceil_max_y; curr_y < size_y; ++curr_y) { + double max_dist = coords_y[curr_y] - source_max; + if (max_dist >= source_move) { + // if we reach here, then we will never have a case where + // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1] + // thus, we can return immediately + + // this optimization is important since this loop is bounded by size, and _not_ by + // a calculated max index based off of source_move - so it would be possible to check + // the whole intersected shape for collisions when we didn't need to! + return source_move; + } + if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON + max_dist = Math.min(max_dist, source_move); + } + for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { + for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { + final int index = curr_z + curr_y*size_z + curr_x*mul_x; + // note: JLS states long shift operators ANDS shift by 63 + if ((bitset[index >>> 6] & (1L << index)) != 0L) { + return max_dist; + } + } + } + } + + return source_move; + } else { + final double source_min = source.minY - off_y; + final int floor_min_y = findFloor( + coords_y, source_min + COLLISION_EPSILON, 0, size_y + ); + + // note: only the order of the first loop matters + + // note: we cannot collide with the face at index 0 on the collision axis for backwards movement + + // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the + // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1] + // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid + final int mul_x = size_y*size_z; + for (int curr_y = floor_min_y - 1; curr_y >= 0; --curr_y) { + double max_dist = coords_y[curr_y + 1] - source_min; + if (max_dist <= source_move) { + // if we reach here, then we will never have a case where + // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1] + // thus, we can return immediately + + // this optimization is important since this loop is possibly bounded by size, and _not_ by + // a calculated max index based off of source_move - so it would be possible to check + // the whole intersected shape for collisions when we didn't need to! + return source_move; + } + if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON + max_dist = Math.max(max_dist, source_move); + } + for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { + for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { + final int index = curr_z + curr_y*size_z + curr_x*mul_x; + // note: JLS states long shift operators ANDS shift by 63 + if ((bitset[index >>> 6] & (1L << index)) != 0L) { + return max_dist; + } + } + } + } + + return source_move; + } + } + + public static double collideZ(final VoxelShape target, final AABB source, final double source_move) { + final AABB single_aabb = ((CollisionVoxelShape)target).getSingleAABBRepresentation(); + if (single_aabb != null) { + return collideZ(single_aabb, source, source_move); + } + // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true + + // offsets that should be applied to coords + final double off_x = ((CollisionVoxelShape)target).offsetX(); + final double off_y = ((CollisionVoxelShape)target).offsetY(); + final double off_z = ((CollisionVoxelShape)target).offsetZ(); + + final double[] coords_x = ((CollisionVoxelShape)target).rootCoordinatesX(); + final double[] coords_y = ((CollisionVoxelShape)target).rootCoordinatesY(); + final double[] coords_z = ((CollisionVoxelShape)target).rootCoordinatesZ(); + + final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); + final int size_y = cached_shape_data.sizeY(); + final int size_z = cached_shape_data.sizeZ(); + + // note: voxel bitset with set index (x, y, z) indicates that + // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) + // is collidable. this is the fundamental principle of operation for the voxel collision operation + + + // note: we should be offsetting coords, but we can also just subtract from source as well - which is + // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) + // note: for intersection, one we find the floor of the min we can use that as the start index + // for the next check as source max >= source min + // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, + // as this implies that coords[coords.length - 1] < source min + // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max + + final int floor_min_x = Math.max( + 0, + findFloor(coords_x, (source.minX - off_x) + COLLISION_EPSILON, 0, size_x) + ); + if (floor_min_x >= size_x) { + // cannot intersect + return source_move; + } + + final int ceil_max_x = Math.min( + size_x, + findFloor(coords_x, (source.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1 + ); + if (floor_min_x >= ceil_max_x) { + // cannot intersect + return source_move; + } + + final int floor_min_y = Math.max( + 0, + findFloor(coords_y, (source.minY - off_y) + COLLISION_EPSILON, 0, size_y) + ); + if (floor_min_y >= size_y) { + // cannot intersect + return source_move; + } + + final int ceil_max_y = Math.min( + size_y, + findFloor(coords_y, (source.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1 + ); + if (floor_min_y >= ceil_max_y) { + // cannot intersect + return source_move; + } + + // index = z + y*size_z + x*(size_z*size_y) + + final long[] bitset = cached_shape_data.voxelSet(); + + if (source_move > 0.0) { + final double source_max = source.maxZ - off_z; + final int ceil_max_z = findFloor( + coords_z, source_max - COLLISION_EPSILON, 0, size_z + ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max + + // note: only the order of the first loop matters + + // note: we cannot collide with the face at index size on the collision axis for forward movement + + final int mul_x = size_y*size_z; + for (int curr_z = ceil_max_z; curr_z < size_z; ++curr_z) { + double max_dist = coords_z[curr_z] - source_max; + if (max_dist >= source_move) { + // if we reach here, then we will never have a case where + // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1] + // thus, we can return immediately + + // this optimization is important since this loop is bounded by size, and _not_ by + // a calculated max index based off of source_move - so it would be possible to check + // the whole intersected shape for collisions when we didn't need to! + return source_move; + } + if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON + max_dist = Math.min(max_dist, source_move); + } + for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { + for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { + final int index = curr_z + curr_y*size_z + curr_x*mul_x; + // note: JLS states long shift operators ANDS shift by 63 + if ((bitset[index >>> 6] & (1L << index)) != 0L) { + return max_dist; + } + } + } + } + + return source_move; + } else { + final double source_min = source.minZ - off_z; + final int floor_min_z = findFloor( + coords_z, source_min + COLLISION_EPSILON, 0, size_z + ); + + // note: only the order of the first loop matters + + // note: we cannot collide with the face at index 0 on the collision axis for backwards movement + + // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the + // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1] + // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid + final int mul_x = size_y*size_z; + for (int curr_z = floor_min_z - 1; curr_z >= 0; --curr_z) { + double max_dist = coords_z[curr_z + 1] - source_min; + if (max_dist <= source_move) { + // if we reach here, then we will never have a case where + // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1] + // thus, we can return immediately + + // this optimization is important since this loop is possibly bounded by size, and _not_ by + // a calculated max index based off of source_move - so it would be possible to check + // the whole intersected shape for collisions when we didn't need to! + return source_move; + } + if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON + max_dist = Math.max(max_dist, source_move); + } + for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { + for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { + final int index = curr_z + curr_y*size_z + curr_x*mul_x; + // note: JLS states long shift operators ANDS shift by 63 + if ((bitset[index >>> 6] & (1L << index)) != 0L) { + return max_dist; + } + } + } + } + + return source_move; + } + } + + // does not use epsilon + public static boolean strictlyContains(final VoxelShape voxel, final Vec3 point) { + return strictlyContains(voxel, point.x, point.y, point.z); + } + + // does not use epsilon + public static boolean strictlyContains(final VoxelShape voxel, double x, double y, double z) { + final AABB single_aabb = ((CollisionVoxelShape)voxel).getSingleAABBRepresentation(); + if (single_aabb != null) { + return single_aabb.contains(x, y, z); + } + + if (voxel.isEmpty()) { + // bitset is clear, no point in searching + return false; + } + + // offset input + x -= ((CollisionVoxelShape)voxel).offsetX(); + y -= ((CollisionVoxelShape)voxel).offsetY(); + z -= ((CollisionVoxelShape)voxel).offsetZ(); + + final double[] coords_x = ((CollisionVoxelShape)voxel).rootCoordinatesX(); + final double[] coords_y = ((CollisionVoxelShape)voxel).rootCoordinatesY(); + final double[] coords_z = ((CollisionVoxelShape)voxel).rootCoordinatesZ(); + + final CachedShapeData cached_shape_data = ((CollisionVoxelShape)voxel).getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); + final int size_y = cached_shape_data.sizeY(); + final int size_z = cached_shape_data.sizeZ(); + + // note: should mirror AABB#contains, which is that for any point X that X >= min and X < max. + // specifically, it cannot collide on the max bounds of the shape + + final int index_x = findFloor(coords_x, x, 0, size_x); + if (index_x < 0 || index_x >= size_x) { + return false; + } + + final int index_y = findFloor(coords_y, y, 0, size_y); + if (index_y < 0 || index_y >= size_y) { + return false; + } + + final int index_z = findFloor(coords_z, z, 0, size_z); + if (index_z < 0 || index_z >= size_z) { + return false; + } + + // index = z + y*size_z + x*(size_z*size_y) + + final int index = index_z + index_y*size_z + index_x*(size_z*size_y); + + final long[] bitset = cached_shape_data.voxelSet(); + + return (bitset[index >>> 6] & (1L << index)) != 0L; + } + + private static int makeBitset(final boolean ft, final boolean tf, final boolean tt) { + // idx ff -> 0 + // idx ft -> 1 + // idx tf -> 2 + // idx tt -> 3 + return ((ft ? 1 : 0) << 1) | ((tf ? 1 : 0) << 2) | ((tt ? 1 : 0) << 3); + } + + private static BitSetDiscreteVoxelShape merge(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond, + final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY, + final MergedVoxelCoordinateList mergedZ, + final int booleanOp) { + final int sizeX = mergedX.voxels; + final int sizeY = mergedY.voxels; + final int sizeZ = mergedZ.voxels; + + final long[] s1Voxels = shapeDataFirst.voxelSet(); + final long[] s2Voxels = shapeDataSecond.voxelSet(); + + final int s1Mul1 = shapeDataFirst.sizeZ(); + final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY(); + + final int s2Mul1 = shapeDataSecond.sizeZ(); + final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY(); + + // note: indices may contain -1, but nothing > size + final BitSetDiscreteVoxelShape ret = new BitSetDiscreteVoxelShape(sizeX, sizeY, sizeZ); + + boolean empty = true; + + int mergedIdx = 0; + for (int idxX = 0; idxX < sizeX; ++idxX) { + final int s1x = mergedX.firstIndices[idxX]; + final int s2x = mergedX.secondIndices[idxX]; + boolean setX = false; + for (int idxY = 0; idxY < sizeY; ++idxY) { + final int s1y = mergedY.firstIndices[idxY]; + final int s2y = mergedY.secondIndices[idxY]; + boolean setY = false; + for (int idxZ = 0; idxZ < sizeZ; ++idxZ) { + final int s1z = mergedZ.firstIndices[idxZ]; + final int s2z = mergedZ.secondIndices[idxZ]; + + int idx; + + final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L); + final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L); + + // idx ff -> 0 + // idx ft -> 1 + // idx tf -> 2 + // idx tt -> 3 + + final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0; + setY |= res; + setX |= res; + + if (res) { + empty = false; + // inline and optimize fill operation + ret.zMin = Math.min(ret.zMin, idxZ); + ret.zMax = Math.max(ret.zMax, idxZ + 1); + ret.storage.set(mergedIdx); + } + + ++mergedIdx; + } + if (setY) { + ret.yMin = Math.min(ret.yMin, idxY); + ret.yMax = Math.max(ret.yMax, idxY + 1); + } + } + if (setX) { + ret.xMin = Math.min(ret.xMin, idxX); + ret.xMax = Math.max(ret.xMax, idxX + 1); + } + } + + return empty ? null : ret; + } + + private static boolean isMergeEmpty(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond, + final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY, + final MergedVoxelCoordinateList mergedZ, + final int booleanOp) { + final int sizeX = mergedX.voxels; + final int sizeY = mergedY.voxels; + final int sizeZ = mergedZ.voxels; + + final long[] s1Voxels = shapeDataFirst.voxelSet(); + final long[] s2Voxels = shapeDataSecond.voxelSet(); + + final int s1Mul1 = shapeDataFirst.sizeZ(); + final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY(); + + final int s2Mul1 = shapeDataSecond.sizeZ(); + final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY(); + + // note: indices may contain -1, but nothing > size + for (int idxX = 0; idxX < sizeX; ++idxX) { + final int s1x = mergedX.firstIndices[idxX]; + final int s2x = mergedX.secondIndices[idxX]; + for (int idxY = 0; idxY < sizeY; ++idxY) { + final int s1y = mergedY.firstIndices[idxY]; + final int s2y = mergedY.secondIndices[idxY]; + for (int idxZ = 0; idxZ < sizeZ; ++idxZ) { + final int s1z = mergedZ.firstIndices[idxZ]; + final int s2z = mergedZ.secondIndices[idxZ]; + + int idx; + + final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L); + final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L); + + // idx ff -> 0 + // idx ft -> 1 + // idx tf -> 2 + // idx tt -> 3 + + final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0; + + if (res) { + return false; + } + } + } + } + + return true; + } + + public static VoxelShape joinOptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { + return joinUnoptimized(first, second, operator).optimize(); + } + + public static VoxelShape joinUnoptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { + final boolean ff = operator.apply(false, false); + if (ff) { + // technically, should be an infinite box but that's clearly an error + throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true"); + } + + final boolean tt = operator.apply(true, true); + + if (first == second) { + return tt ? first : Shapes.empty(); + } + + final boolean ft = operator.apply(false, true); + final boolean tf = operator.apply(true, false); + + if (first.isEmpty()) { + return ft ? second : Shapes.empty(); + } + if (second.isEmpty()) { + return tf ? first : Shapes.empty(); + } + + if (!tt) { + // try to check for no intersection, since tt = false + final AABB aabbF = ((CollisionVoxelShape)first).getSingleAABBRepresentation(); + final AABB aabbS = ((CollisionVoxelShape)second).getSingleAABBRepresentation(); + + final boolean intersect; + + final boolean hasAABBF = aabbF != null; + final boolean hasAABBS = aabbS != null; + if (hasAABBF | hasAABBS) { + if (hasAABBF & hasAABBS) { + intersect = voxelShapeIntersect(aabbF, aabbS); + } else if (hasAABBF) { + intersect = voxelShapeIntersectNoEmpty(second, aabbF); + } else { + intersect = voxelShapeIntersectNoEmpty(first, aabbS); + } + } else { + // expect cached bounds + intersect = voxelShapeIntersect(first.bounds(), second.bounds()); + } + + if (!intersect) { + if (!tf & !ft) { + return Shapes.empty(); + } + if (!tf | !ft) { + return tf ? first : second; + } + } + } + + final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge( + ((CollisionVoxelShape)first).rootCoordinatesX(), ((CollisionVoxelShape)first).offsetX(), + ((CollisionVoxelShape)second).rootCoordinatesX(), ((CollisionVoxelShape)second).offsetX(), + ft, tf + ); + if (mergedX == MergedVoxelCoordinateList.EMPTY) { + return Shapes.empty(); + } + final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge( + ((CollisionVoxelShape)first).rootCoordinatesY(), ((CollisionVoxelShape)first).offsetY(), + ((CollisionVoxelShape)second).rootCoordinatesY(), ((CollisionVoxelShape)second).offsetY(), + ft, tf + ); + if (mergedY == MergedVoxelCoordinateList.EMPTY) { + return Shapes.empty(); + } + final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge( + ((CollisionVoxelShape)first).rootCoordinatesZ(), ((CollisionVoxelShape)first).offsetZ(), + ((CollisionVoxelShape)second).rootCoordinatesZ(), ((CollisionVoxelShape)second).offsetZ(), + ft, tf + ); + if (mergedZ == MergedVoxelCoordinateList.EMPTY) { + return Shapes.empty(); + } + + final CachedShapeData shapeDataFirst = ((CollisionVoxelShape)first).getCachedVoxelData(); + final CachedShapeData shapeDataSecond = ((CollisionVoxelShape)second).getCachedVoxelData(); + + final BitSetDiscreteVoxelShape mergedShape = merge( + shapeDataFirst, shapeDataSecond, + mergedX, mergedY, mergedZ, + makeBitset(ft, tf, tt) + ); + + if (mergedShape == null) { + return Shapes.empty(); + } + + return new ArrayVoxelShape( + mergedShape, mergedX.wrapCoords(), mergedY.wrapCoords(), mergedZ.wrapCoords() + ); + } + + public static boolean isJoinNonEmpty(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { + final boolean ff = operator.apply(false, false); + if (ff) { + // technically, should be an infinite box but that's clearly an error + throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true"); + } + final boolean firstEmpty = first.isEmpty(); + final boolean secondEmpty = second.isEmpty(); + if (firstEmpty | secondEmpty) { + return operator.apply(!firstEmpty, !secondEmpty); + } + + final boolean tt = operator.apply(true, true); + + if (first == second) { + return tt; + } + + final boolean ft = operator.apply(false, true); + final boolean tf = operator.apply(true, false); + + // try to check intersection + final AABB aabbF = ((CollisionVoxelShape)first).getSingleAABBRepresentation(); + final AABB aabbS = ((CollisionVoxelShape)second).getSingleAABBRepresentation(); + + final boolean intersect; + + final boolean hasAABBF = aabbF != null; + final boolean hasAABBS = aabbS != null; + if (hasAABBF | hasAABBS) { + if (hasAABBF & hasAABBS) { + intersect = voxelShapeIntersect(aabbF, aabbS); + } else if (hasAABBF) { + intersect = voxelShapeIntersectNoEmpty(second, aabbF); + } else { + // hasAABBS -> true + intersect = voxelShapeIntersectNoEmpty(first, aabbS); + } + + if (!intersect) { + // is only non-empty if we take from first or second, as there is no overlap AND both shapes are non-empty + return tf | ft; + } else if (tt) { + // intersect = true && tt = true -> non-empty merged shape + return true; + } + } else { + // expect cached bounds + intersect = voxelShapeIntersect(first.bounds(), second.bounds()); + if (!intersect) { + // is only non-empty if we take from first or second, as there is no intersection + return tf | ft; + } + } + + final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge( + ((CollisionVoxelShape)first).rootCoordinatesX(), ((CollisionVoxelShape)first).offsetX(), + ((CollisionVoxelShape)second).rootCoordinatesX(), ((CollisionVoxelShape)second).offsetX(), + ft, tf + ); + if (mergedX == MergedVoxelCoordinateList.EMPTY) { + return false; + } + final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge( + ((CollisionVoxelShape)first).rootCoordinatesY(), ((CollisionVoxelShape)first).offsetY(), + ((CollisionVoxelShape)second).rootCoordinatesY(), ((CollisionVoxelShape)second).offsetY(), + ft, tf + ); + if (mergedY == MergedVoxelCoordinateList.EMPTY) { + return false; + } + final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge( + ((CollisionVoxelShape)first).rootCoordinatesZ(), ((CollisionVoxelShape)first).offsetZ(), + ((CollisionVoxelShape)second).rootCoordinatesZ(), ((CollisionVoxelShape)second).offsetZ(), + ft, tf + ); + if (mergedZ == MergedVoxelCoordinateList.EMPTY) { + return false; + } + + final CachedShapeData shapeDataFirst = ((CollisionVoxelShape)first).getCachedVoxelData(); + final CachedShapeData shapeDataSecond = ((CollisionVoxelShape)second).getCachedVoxelData(); + + return !isMergeEmpty( + shapeDataFirst, shapeDataSecond, + mergedX, mergedY, mergedZ, + makeBitset(ft, tf, tt) + ); + } + + private static final class MergedVoxelCoordinateList { + + private static final int[][] SIMPLE_INDICES_CACHE = new int[64][]; + static { + for (int i = 0; i < SIMPLE_INDICES_CACHE.length; ++i) { + SIMPLE_INDICES_CACHE[i] = getIndices(i); + } + } + + private static final MergedVoxelCoordinateList EMPTY = new MergedVoxelCoordinateList( + new double[] { 0.0 }, 0.0, new int[0], new int[0], 0 + ); + + private static int[] getIndices(final int length) { + final int[] ret = new int[length]; + + for (int i = 1; i < length; ++i) { + ret[i] = i; + } + + return ret; + } + + // indices above voxel size are always set to -1 + public final double[] coordinates; + public final double coordinateOffset; + public final int[] firstIndices; + public final int[] secondIndices; + public final int voxels; + + private MergedVoxelCoordinateList(final double[] coordinates, final double coordinateOffset, + final int[] firstIndices, final int[] secondIndices, final int voxels) { + this.coordinates = coordinates; + this.coordinateOffset = coordinateOffset; + this.firstIndices = firstIndices; + this.secondIndices = secondIndices; + this.voxels = voxels; + } + + public DoubleList wrapCoords() { + if (this.coordinateOffset == 0.0) { + return DoubleArrayList.wrap(this.coordinates, this.voxels + 1); + } + return new OffsetDoubleList(DoubleArrayList.wrap(this.coordinates, this.voxels + 1), this.coordinateOffset); + } + + // assume coordinates.length > 1 + public static MergedVoxelCoordinateList getForSingle(final double[] coordinates, final double offset) { + final int voxels = coordinates.length - 1; + final int[] indices = voxels < SIMPLE_INDICES_CACHE.length ? SIMPLE_INDICES_CACHE[voxels] : getIndices(voxels); + + return new MergedVoxelCoordinateList(coordinates, offset, indices, indices, voxels); + } + + // assume coordinates.length > 1 + public static MergedVoxelCoordinateList merge(final double[] firstCoordinates, final double firstOffset, + final double[] secondCoordinates, final double secondOffset, + final boolean ft, final boolean tf) { + if (firstCoordinates == secondCoordinates && firstOffset == secondOffset) { + return getForSingle(firstCoordinates, firstOffset); + } + + final int firstCount = firstCoordinates.length; + final int secondCount = secondCoordinates.length; + + final int voxelsFirst = firstCount - 1; + final int voxelsSecond = secondCount - 1; + + final int maxCount = firstCount + secondCount; + + final double[] coordinates = new double[maxCount]; + final int[] firstIndices = new int[maxCount]; + final int[] secondIndices = new int[maxCount]; + + final boolean notTF = !tf; + final boolean notFT = !ft; + + int firstIndex = 0; + int secondIndex = 0; + int resultSize = 0; + + // note: operations on NaN are false + double last = Double.NaN; + + for (;;) { + final boolean noneLeftFirst = firstIndex >= firstCount; + final boolean noneLeftSecond = secondIndex >= secondCount; + + if ((noneLeftFirst & noneLeftSecond) | (noneLeftSecond & notTF) | (noneLeftFirst & notFT)) { + break; + } + + final boolean firstZero = firstIndex == 0; + final boolean secondZero = secondIndex == 0; + + final double select; + + if (noneLeftFirst) { + // noneLeftSecond -> false + // notFT -> false + select = secondCoordinates[secondIndex] + secondOffset; + ++secondIndex; + } else if (noneLeftSecond) { + // noneLeftFirst -> false + // notTF -> false + select = firstCoordinates[firstIndex] + firstOffset; + ++firstIndex; + } else { + // noneLeftFirst | noneLeftSecond -> false + // notTF -> ?? + // notFT -> ?? + final boolean breakFirst = notTF & secondZero; + final boolean breakSecond = notFT & firstZero; + + final double first = firstCoordinates[firstIndex] + firstOffset; + final double second = secondCoordinates[secondIndex] + secondOffset; + final boolean useFirst = first < (second + COLLISION_EPSILON); + final boolean cont = (useFirst & breakFirst) | (!useFirst & breakSecond); + + select = useFirst ? first : second; + firstIndex += useFirst ? 1 : 0; + secondIndex += 1 ^ (useFirst ? 1 : 0); + + if (cont) { + continue; + } + } + + int prevFirst = firstIndex - 1; + prevFirst = prevFirst >= voxelsFirst ? -1 : prevFirst; + int prevSecond = secondIndex - 1; + prevSecond = prevSecond >= voxelsSecond ? -1 : prevSecond; + + if (last >= (select - COLLISION_EPSILON)) { + // note: any operations on NaN is false + firstIndices[resultSize - 1] = prevFirst; + secondIndices[resultSize - 1] = prevSecond; + } else { + firstIndices[resultSize] = prevFirst; + secondIndices[resultSize] = prevSecond; + coordinates[resultSize] = select; + + ++resultSize; + last = select; + } + } + + return resultSize <= 1 ? EMPTY : new MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1); + } + } + + public static boolean equals(final DiscreteVoxelShape shape1, final DiscreteVoxelShape shape2) { + final CachedShapeData cachedShapeData1 = ((CollisionDiscreteVoxelShape)shape1).getOrCreateCachedShapeData(); + final CachedShapeData cachedShapeData2 = ((CollisionDiscreteVoxelShape)shape2).getOrCreateCachedShapeData(); + + final boolean isEmpty1 = cachedShapeData1.isEmpty(); + final boolean isEmpty2 = cachedShapeData2.isEmpty(); + + if (isEmpty1 & isEmpty2) { + return true; + } else if (isEmpty1 ^ isEmpty2) { + return false; + } + + if (cachedShapeData1.hasSingleAABB() != cachedShapeData2.hasSingleAABB()) { + return false; + } + + if (cachedShapeData1.sizeX() != cachedShapeData2.sizeX()) { + return false; + } + if (cachedShapeData1.sizeY() != cachedShapeData2.sizeY()) { + return false; + } + if (cachedShapeData1.sizeZ() != cachedShapeData2.sizeZ()) { + return false; + } + + return Arrays.equals(cachedShapeData1.voxelSet(), cachedShapeData2.voxelSet()); + } + + // useful only for testing + public static boolean equals(final VoxelShape shape1, final VoxelShape shape2) { + if (!equals(shape1.shape, shape2.shape)) { + return false; + } + + return shape1.getCoords(Direction.Axis.X).equals(shape2.getCoords(Direction.Axis.X)) && + shape1.getCoords(Direction.Axis.Y).equals(shape2.getCoords(Direction.Axis.Y)) && + shape1.getCoords(Direction.Axis.Z).equals(shape2.getCoords(Direction.Axis.Z)); + } + + public static AABB offsetX(final AABB box, final double dx) { + return new AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); + } + + public static AABB offsetY(final AABB box, final double dy) { + return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ); + } + + public static AABB offsetZ(final AABB box, final double dz) { + return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz); + } + + public static AABB expandRight(final AABB box, final double dx) { // dx > 0.0 + return new AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); + } + + public static AABB expandLeft(final AABB box, final double dx) { // dx < 0.0 + return new AABB(box.minX - dx, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ); + } + + public static AABB expandUpwards(final AABB box, final double dy) { // dy > 0.0 + return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, box.maxZ); + } + + public static AABB expandDownwards(final AABB box, final double dy) { // dy < 0.0 + return new AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, box.maxZ); + } + + public static AABB expandForwards(final AABB box, final double dz) { // dz > 0.0 + return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz); + } + + public static AABB expandBackwards(final AABB box, final double dz) { // dz < 0.0 + return new AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ); + } + + public static AABB cutRight(final AABB box, final double dx) { // dx > 0.0 + return new AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); + } + + public static AABB cutLeft(final AABB box, final double dx) { // dx < 0.0 + return new AABB(box.minX + dx, box.minY, box.minZ, box.minX, box.maxY, box.maxZ); + } + + public static AABB cutUpwards(final AABB box, final double dy) { // dy > 0.0 + return new AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, box.maxZ); + } + + public static AABB cutDownwards(final AABB box, final double dy) { // dy < 0.0 + return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, box.maxZ); + } + + public static AABB cutForwards(final AABB box, final double dz) { // dz > 0.0 + return new AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz); + } + + public static AABB cutBackwards(final AABB box, final double dz) { // dz < 0.0 + return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ); + } + + public static double performAABBCollisionsX(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + if (Math.abs(value) < COLLISION_EPSILON) { + return 0.0; + } + final AABB target = potentialCollisions.get(i); + value = collideX(target, currentBoundingBox, value); + } + + return value; + } + + public static double performAABBCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + if (Math.abs(value) < COLLISION_EPSILON) { + return 0.0; + } + final AABB target = potentialCollisions.get(i); + value = collideY(target, currentBoundingBox, value); + } + + return value; + } + + public static double performAABBCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + if (Math.abs(value) < COLLISION_EPSILON) { + return 0.0; + } + final AABB target = potentialCollisions.get(i); + value = collideZ(target, currentBoundingBox, value); + } + + return value; + } + + public static double performVoxelCollisionsX(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + if (Math.abs(value) < COLLISION_EPSILON) { + return 0.0; + } + final VoxelShape target = potentialCollisions.get(i); + value = collideX(target, currentBoundingBox, value); + } + + return value; + } + + public static double performVoxelCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + if (Math.abs(value) < COLLISION_EPSILON) { + return 0.0; + } + final VoxelShape target = potentialCollisions.get(i); + value = collideY(target, currentBoundingBox, value); + } + + return value; + } + + public static double performVoxelCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + if (Math.abs(value) < COLLISION_EPSILON) { + return 0.0; + } + final VoxelShape target = potentialCollisions.get(i); + value = collideZ(target, currentBoundingBox, value); + } + + return value; + } + + public static Vec3 performVoxelCollisions(final Vec3 moveVector, AABB axisalignedbb, final List potentialCollisions) { + double x = moveVector.x; + double y = moveVector.y; + double z = moveVector.z; + + if (y != 0.0) { + y = performVoxelCollisionsY(axisalignedbb, y, potentialCollisions); + if (y != 0.0) { + axisalignedbb = offsetY(axisalignedbb, y); + } + } + + final boolean xSmaller = Math.abs(x) < Math.abs(z); + + if (xSmaller && z != 0.0) { + z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions); + if (z != 0.0) { + axisalignedbb = offsetZ(axisalignedbb, z); + } + } + + if (x != 0.0) { + x = performVoxelCollisionsX(axisalignedbb, x, potentialCollisions); + if (!xSmaller && x != 0.0) { + axisalignedbb = offsetX(axisalignedbb, x); + } + } + + if (!xSmaller && z != 0.0) { + z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions); + } + + return new Vec3(x, y, z); + } + + public static Vec3 performAABBCollisions(final Vec3 moveVector, AABB axisalignedbb, final List potentialCollisions) { + double x = moveVector.x; + double y = moveVector.y; + double z = moveVector.z; + + if (y != 0.0) { + y = performAABBCollisionsY(axisalignedbb, y, potentialCollisions); + if (y != 0.0) { + axisalignedbb = offsetY(axisalignedbb, y); + } + } + + final boolean xSmaller = Math.abs(x) < Math.abs(z); + + if (xSmaller && z != 0.0) { + z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions); + if (z != 0.0) { + axisalignedbb = offsetZ(axisalignedbb, z); + } + } + + if (x != 0.0) { + x = performAABBCollisionsX(axisalignedbb, x, potentialCollisions); + if (!xSmaller && x != 0.0) { + axisalignedbb = offsetX(axisalignedbb, x); + } + } + + if (!xSmaller && z != 0.0) { + z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions); + } + + return new Vec3(x, y, z); + } + + public static Vec3 performCollisions(final Vec3 moveVector, AABB axisalignedbb, + final List voxels, + final List aabbs) { + if (voxels.isEmpty()) { + // fast track only AABBs + return performAABBCollisions(moveVector, axisalignedbb, aabbs); + } + + double x = moveVector.x; + double y = moveVector.y; + double z = moveVector.z; + + if (y != 0.0) { + y = performAABBCollisionsY(axisalignedbb, y, aabbs); + y = performVoxelCollisionsY(axisalignedbb, y, voxels); + if (y != 0.0) { + axisalignedbb = offsetY(axisalignedbb, y); + } + } + + final boolean xSmaller = Math.abs(x) < Math.abs(z); + + if (xSmaller && z != 0.0) { + z = performAABBCollisionsZ(axisalignedbb, z, aabbs); + z = performVoxelCollisionsZ(axisalignedbb, z, voxels); + if (z != 0.0) { + axisalignedbb = offsetZ(axisalignedbb, z); + } + } + + if (x != 0.0) { + x = performAABBCollisionsX(axisalignedbb, x, aabbs); + x = performVoxelCollisionsX(axisalignedbb, x, voxels); + if (!xSmaller && x != 0.0) { + axisalignedbb = offsetX(axisalignedbb, x); + } + } + + if (!xSmaller && z != 0.0) { + z = performAABBCollisionsZ(axisalignedbb, z, aabbs); + z = performVoxelCollisionsZ(axisalignedbb, z, voxels); + } + + return new Vec3(x, y, z); + } + + public static boolean isAlmostCollidingOnBorder(final WorldBorder worldborder, final AABB boundingBox) { + return isAlmostCollidingOnBorder(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); + } + + public static boolean isAlmostCollidingOnBorder(final WorldBorder worldborder, final double boxMinX, final double boxMaxX, + final double boxMinZ, final double boxMaxZ) { + final double borderMinX = worldborder.getMinX(); // -X + final double borderMaxX = worldborder.getMaxX(); // +X + + final double borderMinZ = worldborder.getMinZ(); // -Z + final double borderMaxZ = worldborder.getMaxZ(); // +Z + + return + // Not intersecting if we're smaller + !voxelShapeIntersect( + boxMinX + COLLISION_EPSILON, Double.NEGATIVE_INFINITY, boxMinZ + COLLISION_EPSILON, + boxMaxX - COLLISION_EPSILON, Double.POSITIVE_INFINITY, boxMaxZ - COLLISION_EPSILON, + borderMinX, Double.NEGATIVE_INFINITY, borderMinZ, borderMaxX, Double.POSITIVE_INFINITY, borderMaxZ + ) + && + + // Are intersecting if we're larger + voxelShapeIntersect( + boxMinX - COLLISION_EPSILON, Double.NEGATIVE_INFINITY, boxMinZ - COLLISION_EPSILON, + boxMaxX + COLLISION_EPSILON, Double.POSITIVE_INFINITY, boxMaxZ + COLLISION_EPSILON, + borderMinX, Double.NEGATIVE_INFINITY, borderMinZ, borderMaxX, Double.POSITIVE_INFINITY, borderMaxZ + ); + } + + public static boolean isCollidingWithBorderEdge(final WorldBorder worldborder, final AABB boundingBox) { + return isCollidingWithBorderEdge(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); + } + + public static boolean isCollidingWithBorderEdge(final WorldBorder worldborder, final double boxMinX, final double boxMaxX, + final double boxMinZ, final double boxMaxZ) { + final double borderMinX = worldborder.getMinX() + COLLISION_EPSILON; // -X + final double borderMaxX = worldborder.getMaxX() - COLLISION_EPSILON; // +X + + final double borderMinZ = worldborder.getMinZ() + COLLISION_EPSILON; // -Z + final double borderMaxZ = worldborder.getMaxZ() - COLLISION_EPSILON; // +Z + + return boxMinX < borderMinX || boxMaxX > borderMaxX || boxMinZ < borderMinZ || boxMaxZ > borderMaxZ; + } + + /* Math.max/min specify that any NaN argument results in a NaN return, unlike these functions */ + private static double min(final double x, final double y) { + return x < y ? x : y; + } + + private static double max(final double x, final double y) { + return x > y ? x : y; + } + + /** + * @param box Bounding volume to check + * @param fromX Starting x-coordinate of ray + * @param fromY Starting y-coordinate of ray + * @param fromZ Starting z-coordinate of ray + * @param directionInvX 1.0 / ray x-direction + * @param directionInvY 1.0 / ray y-direction + * @param directionInvZ 1.0 / ray z-direction + * @param tMax Maximum distance along ray direction that the ray can clip + */ + public static boolean clips(final AABB box, + final double fromX, final double fromY, final double fromZ, + final double directionInvX, final double directionInvY, final double directionInvZ, + double tMax) { + /* https://tavianator.com/2022/ray_box_boundary.html */ + + double tMin = 0.0; + + double t1, t2; + + t1 = (box.minX - fromX) * directionInvX; + t2 = (box.maxX - fromX) * directionInvX; + + tMin = min(max(t1, tMin), max(t2, tMin)); + tMax = max(min(t1, tMax), min(t2, tMax)); + + t1 = (box.minY - fromY) * directionInvY; + t2 = (box.maxY - fromY) * directionInvY; + + tMin = min(max(t1, tMin), max(t2, tMin)); + tMax = max(min(t1, tMax), min(t2, tMax)); + + t1 = (box.minZ - fromZ) * directionInvZ; + t2 = (box.maxZ - fromZ) * directionInvZ; + + tMin = min(max(t1, tMin), max(t2, tMin)); + tMax = max(min(t1, tMax), min(t2, tMax)); + + return tMin <= tMax; + } + + public static final int COLLISION_FLAG_LOAD_CHUNKS = 1 << 0; + public static final int COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS = 1 << 1; + public static final int COLLISION_FLAG_CHECK_BORDER = 1 << 2; + public static final int COLLISION_FLAG_CHECK_ONLY = 1 << 3; + + public static boolean getCollisionsForBlocksOrWorldBorder(final Level world, final Entity entity, final AABB aabb, + final List intoVoxel, final List intoAABB, + final int collisionFlags, final BiPredicate predicate) { + final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; + boolean ret = false; + + if ((collisionFlags & COLLISION_FLAG_CHECK_BORDER) != 0) { + if (CollisionUtil.isCollidingWithBorderEdge(world.getWorldBorder(), aabb)) { + if (checkOnly) { + return true; + } else { + // the collision shape is basically always voxel, don't check it + final VoxelShape borderShape = world.getWorldBorder().getCollisionShape(); + intoVoxel.add(borderShape); + ret = true; + } + } + } + + final int minSection = ((CollisionLevel)world).getMinSectionMoonrise(); + + final int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1; + final int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1; + + final int minBlockY = Math.max((minSection << 4) - 1, Mth.floor(aabb.minY - COLLISION_EPSILON) - 1); + final int maxBlockY = Math.min((((CollisionLevel)world).getMaxSectionMoonrise() << 4) + 16, Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1); + + final int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1; + final int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1; + + final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); + final CollisionContext collisionShape = new LazyEntityCollisionContext(entity); + + // special cases: + if (minBlockY > maxBlockY) { + // no point in checking + return ret; + } + + final int minChunkX = minBlockX >> 4; + final int maxChunkX = maxBlockX >> 4; + + final int minChunkY = minBlockY >> 4; + final int maxChunkY = maxBlockY >> 4; + + final int minChunkZ = minBlockZ >> 4; + final int maxChunkZ = maxBlockZ >> 4; + + final boolean loadChunks = (collisionFlags & COLLISION_FLAG_LOAD_CHUNKS) != 0; + + for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { + for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { + final ChunkAccess chunk = (ChunkAccess)world.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks); + + if (chunk == null) { + if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) { + if (checkOnly) { + return true; + } else { + intoAABB.add(getBoxForChunk(currChunkX, currChunkZ)); + ret = true; + } + } + continue; + } + + final LevelChunkSection[] sections = chunk.getSections(); + + // bound y + for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { + final int sectionIdx = currChunkY - minSection; + if (sectionIdx < 0 || sectionIdx >= sections.length) { + continue; + } + final LevelChunkSection section = sections[sectionIdx]; + if (section == null || section.hasOnlyAir()) { + // empty + continue; + } + final PalettedContainer blocks = section.states; + + final boolean hasSpecial = ((CollisionLevelChunkSection)section).getSpecialCollidingBlocks() != 0; + final int sectionAdjust = !hasSpecial ? 1 : 0; + + final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0; + final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15; + final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) + sectionAdjust : 0; + final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) - sectionAdjust : 15; + final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) + sectionAdjust : 0; + final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) - sectionAdjust : 15; + + for (int currY = minYIterate; currY <= maxYIterate; ++currY) { + final int blockY = currY | (currChunkY << 4); + for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { + final int blockZ = currZ | (currChunkZ << 4); + for (int currX = minXIterate; currX <= maxXIterate; ++currX) { + final int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8); + final int blockX = currX | (currChunkX << 4); + + final int edgeCount = hasSpecial ? ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + + ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + + ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0) : 0; + if (edgeCount == 3) { + continue; + } + + final BlockState blockData = blocks.get(localBlockIndex); + + if (((CollisionBlockState)blockData).emptyCollisionShape()) { + continue; + } + + if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) { + VoxelShape blockCollision = ((CollisionBlockState)blockData).getConstantCollisionShape(); + + if (blockCollision == null) { + mutablePos.set(blockX, blockY, blockZ); + blockCollision = blockData.getCollisionShape(world, mutablePos, collisionShape); + } + + if (blockCollision.isEmpty()) { + continue; + } + + AABB singleAABB = ((CollisionVoxelShape)blockCollision).getSingleAABBRepresentation(); + if (singleAABB != null) { + singleAABB = singleAABB.move((double)blockX, (double)blockY, (double)blockZ); + if (!voxelShapeIntersect(aabb, singleAABB)) { + continue; + } + + if (predicate != null) { + mutablePos.set(blockX, blockY, blockZ); + if (!predicate.test(blockData, mutablePos)) { + continue; + } + } + + if (checkOnly) { + return true; + } else { + ret = true; + intoAABB.add(singleAABB); + continue; + } + } + + final VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ); + + if (!voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) { + continue; + } + + if (predicate != null) { + mutablePos.set(blockX, blockY, blockZ); + if (!predicate.test(blockData, mutablePos)) { + continue; + } + } + + if (checkOnly) { + return true; + } else { + ret = true; + intoVoxel.add(blockCollisionOffset); + continue; + } + } + } + } + } + } + } + } + + return ret; + } + + public static boolean getEntityHardCollisions(final CollisionGetter getter, final Entity entity, AABB aabb, + final List into, final int collisionFlags, final Predicate predicate) { + final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; + if (!(getter instanceof EntityGetter entityGetter)) { + return false; + } + + boolean ret = false; + + // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with. + // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems + // specifically with boat collisions. + aabb = aabb.inflate(-COLLISION_EPSILON, -COLLISION_EPSILON, -COLLISION_EPSILON); + final List entities; + if (entity != null && ((CollisionEntity)entity).isHardColliding()) { + entities = entityGetter.getEntities(entity, aabb, predicate); + } else { + entities = ((CollisionEntityGetter)entityGetter).getHardCollidingEntities(entity, aabb, predicate); + } + + for (int i = 0, len = entities.size(); i < len; ++i) { + final Entity otherEntity = entities.get(i); + + if (otherEntity.isSpectator()) { + continue; + } + + if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) { + if (checkOnly) { + return true; + } else { + into.add(otherEntity.getBoundingBox()); + ret = true; + } + } + } + + return ret; + } + + public static boolean getCollisions(final Level world, final Entity entity, final AABB aabb, + final List intoVoxel, final List intoAABB, final int collisionFlags, + final BiPredicate blockPredicate, + final Predicate entityPredicate) { + if ((collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0) { + return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate) + || getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate); + } else { + return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate) + | getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate); + } + } + + public static final class LazyEntityCollisionContext extends EntityCollisionContext { + + private CollisionContext delegate; + private boolean delegated; + + public LazyEntityCollisionContext(final Entity entity) { + super(false, 0.0, null, null, entity); + } + + public boolean isDelegated() { + final boolean delegated = this.delegated; + this.delegated = false; + return delegated; + } + + public CollisionContext getDelegate() { + this.delegated = true; + final Entity entity = this.getEntity(); + return this.delegate == null ? this.delegate = (entity == null ? CollisionContext.empty() : CollisionContext.of(entity)) : this.delegate; + } + + @Override + public boolean isDescending() { + return this.getDelegate().isDescending(); + } + + @Override + public boolean isAbove(final VoxelShape shape, final BlockPos pos, final boolean defaultValue) { + return this.getDelegate().isAbove(shape, pos, defaultValue); + } + + @Override + public boolean isHoldingItem(final Item item) { + return this.getDelegate().isHoldingItem(item); + } + + @Override + public boolean canStandOnFluid(final FluidState state, final FluidState fluidState) { + return this.getDelegate().canStandOnFluid(state, fluidState); + } + } + + private CollisionUtil() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java new file mode 100644 index 0000000..e7b60ec --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java @@ -0,0 +1,30 @@ +package ca.spottedleaf.moonrise.patches.collisions.block; + +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.VoxelShape; + +public interface CollisionBlockState { + + // note: this does not consider canOcclude, it is only based on the cached collision shape (i.e hasCache()) + // and whether Shapes.faceShapeOccludes(EMPTY, cached shape) is true + public boolean occludesFullBlock(); + + // whether the cached collision shape exists and is empty + public boolean emptyCollisionShape(); + + // indicates that occludesFullBlock is cached for the collision shape + public boolean hasCache(); + + // note: this is HashCommon#murmurHash3(incremental id); and since murmurHash3 has an inverse function the returned + // value is still unique + public int uniqueId1(); + + // note: this is HashCommon#murmurHash3(incremental id); and since murmurHash3 has an inverse function the returned + // value is still unique + public int uniqueId2(); + + public VoxelShape getConstantCollisionShape(); + + public AABB getConstantCollisionAABB(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/entity/CollisionEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/entity/CollisionEntity.java new file mode 100644 index 0000000..2666d01 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/entity/CollisionEntity.java @@ -0,0 +1,16 @@ +package ca.spottedleaf.moonrise.patches.collisions.entity; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.monster.Shulker; +import net.minecraft.world.entity.vehicle.AbstractMinecart; +import net.minecraft.world.entity.vehicle.Boat; + +public interface CollisionEntity { + + public boolean isHardColliding(); + + // for mods to override + public default boolean isHardCollidingUncached() { + return this instanceof Boat || this instanceof AbstractMinecart || this instanceof Shulker || ((Entity)this).canBeCollidedWith(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java new file mode 100644 index 0000000..5a6b16b --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java @@ -0,0 +1,10 @@ +package ca.spottedleaf.moonrise.patches.collisions.shape; + +public record CachedShapeData( + int sizeX, int sizeY, int sizeZ, + long[] voxelSet, + int minFullX, int minFullY, int minFullZ, + int maxFullX, int maxFullY, int maxFullZ, + boolean isEmpty, boolean hasSingleAABB +) { +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java new file mode 100644 index 0000000..af8ebe5 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java @@ -0,0 +1,37 @@ +package ca.spottedleaf.moonrise.patches.collisions.shape; + +import net.minecraft.world.phys.AABB; +import java.util.ArrayList; +import java.util.List; + +public record CachedToAABBs( + List aabbs, + boolean isOffset, + double offX, double offY, double offZ +) { + + public CachedToAABBs removeOffset() { + final List toOffset = this.aabbs; + final double offX = this.offX; + final double offY = this.offY; + final double offZ = this.offZ; + + final List ret = new ArrayList<>(toOffset.size()); + + for (int i = 0, len = toOffset.size(); i < len; ++i) { + ret.add(toOffset.get(i).move(offX, offY, offZ)); + } + + return new CachedToAABBs(ret, false, 0.0, 0.0, 0.0); + } + + public static CachedToAABBs offset(final CachedToAABBs cache, final double offX, final double offY, final double offZ) { + final double resX = cache.offX + offX; + final double resY = cache.offY + offY; + final double resZ = cache.offZ + offZ; + + final boolean isOffset = resX != 0.0 || resY != 0.0 || resZ != 0.0; + + return new CachedToAABBs(cache.aabbs, isOffset, resX, resY, resZ); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java new file mode 100644 index 0000000..c3f27a9 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java @@ -0,0 +1,7 @@ +package ca.spottedleaf.moonrise.patches.collisions.shape; + +public interface CollisionDiscreteVoxelShape { + + public CachedShapeData getOrCreateCachedShapeData(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java new file mode 100644 index 0000000..626cb5b --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java @@ -0,0 +1,46 @@ +package ca.spottedleaf.moonrise.patches.collisions.shape; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.VoxelShape; + +public interface CollisionVoxelShape { + + public double offsetX(); + + public double offsetY(); + + public double offsetZ(); + + public double[] rootCoordinatesX(); + + public double[] rootCoordinatesY(); + + public double[] rootCoordinatesZ(); + + public CachedShapeData getCachedVoxelData(); + + // rets null if not possible to represent this shape as one AABB + public AABB getSingleAABBRepresentation(); + + // ONLY USE INTERNALLY, ONLY FOR INITIALISING IN CONSTRUCTOR: VOXELSHAPES ARE STATIC + public void initCache(); + + // this returns empty if not clamped to 1.0 or 0.0 depending on direction + public VoxelShape getFaceShapeClamped(final Direction direction); + + public boolean isFullBlock(); + + public boolean doesClip(final double fromX, final double fromY, final double fromZ, + final double directionInvX, final double directionInvY, final double directionInvZ, + final double tMax); + + public boolean occludesFullBlock(); + + public boolean occludesFullBlockIfCached(); + + // uses a cache internally + public VoxelShape orUnoptimized(final VoxelShape other); +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java new file mode 100644 index 0000000..44831fc --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java @@ -0,0 +1,10 @@ +package ca.spottedleaf.moonrise.patches.collisions.shape; + +import net.minecraft.world.phys.shapes.VoxelShape; + +public record MergedORCache( + VoxelShape key, + VoxelShape result +) { + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/slices/ChunkEntitySlices.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/slices/ChunkEntitySlices.java new file mode 100644 index 0000000..4bc10ce --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/slices/ChunkEntitySlices.java @@ -0,0 +1,626 @@ +package ca.spottedleaf.moonrise.patches.collisions.slices; + +import ca.spottedleaf.moonrise.common.list.ReferenceList; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.collisions.entity.CollisionEntity; +import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.boss.EnderDragonPart; +import net.minecraft.world.entity.boss.enderdragon.EnderDragon; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.entity.Visibility; +import net.minecraft.world.phys.AABB; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.function.Predicate; + +public final class ChunkEntitySlices { + + protected final int minSection; + protected final int maxSection; + public final int chunkX; + public final int chunkZ; + protected final Level world; + + protected final EntityCollectionBySection allEntities; + protected final EntityCollectionBySection hardCollidingEntities; + protected final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByType; + protected final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass; + protected final ReferenceList entities = new ReferenceList<>(); + + public Visibility sectionVisibility = Visibility.TRACKED; // TODO + + public ChunkEntitySlices(final Level world, final int chunkX, final int chunkZ) { // inclusive, inclusive + this.minSection = WorldUtil.getMinSection(world); + this.maxSection = WorldUtil.getMaxSection(world); + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.world = world; + + this.allEntities = new EntityCollectionBySection(this); + this.hardCollidingEntities = new EntityCollectionBySection(this); + this.entitiesByType = new Reference2ObjectOpenHashMap<>(); + this.entitiesByClass = new Reference2ObjectOpenHashMap<>(); + } + + public boolean isEmpty() { + return this.entities.size() == 0; + } + + public boolean addEntity(final Entity entity, final int chunkSection) { + if (!this.entities.add(entity)) { + return false; + } + final int sectionIndex = chunkSection - this.minSection; + + this.allEntities.addEntity(entity, sectionIndex); + + if (((CollisionEntity)entity).isHardColliding()) { + this.hardCollidingEntities.addEntity(entity, sectionIndex); + } + + for (final Iterator, EntityCollectionBySection>> iterator = + this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); + + if (entry.getKey().isInstance(entity)) { + entry.getValue().addEntity(entity, sectionIndex); + } + } + + EntityCollectionBySection byType = this.entitiesByType.get(entity.getType()); + if (byType != null) { + byType.addEntity(entity, sectionIndex); + } else { + this.entitiesByType.put(entity.getType(), byType = new EntityCollectionBySection(this)); + byType.addEntity(entity, sectionIndex); + } + + return true; + } + + public boolean removeEntity(final Entity entity, final int chunkSection) { + if (!this.entities.remove(entity)) { + return false; + } + final int sectionIndex = chunkSection - this.minSection; + + this.allEntities.removeEntity(entity, sectionIndex); + + if (((CollisionEntity)entity).isHardColliding()) { + this.hardCollidingEntities.removeEntity(entity, sectionIndex); + } + + for (final Iterator, EntityCollectionBySection>> iterator = + this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); + + if (entry.getKey().isInstance(entity)) { + entry.getValue().removeEntity(entity, sectionIndex); + } + } + + final EntityCollectionBySection byType = this.entitiesByType.get(entity.getType()); + byType.removeEntity(entity, sectionIndex); + + return true; + } + + public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + this.hardCollidingEntities.getEntities(except, box, into, predicate); + } + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate); + } + + public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List into, final Predicate predicate) { + this.allEntities.getEntities(except, box, into, predicate); + } + + + public boolean getEntities(final Entity except, final AABB box, final List into, final Predicate predicate, + final int maxCount) { + return this.allEntities.getEntitiesWithEnderDragonPartsLimited(except, box, into, predicate, maxCount); + } + + public boolean getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List into, final Predicate predicate, + final int maxCount) { + return this.allEntities.getEntitiesLimited(except, box, into, predicate, maxCount); + } + + public void getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate) { + final EntityCollectionBySection byType = this.entitiesByType.get(type); + + if (byType != null) { + byType.getEntities((Entity)null, box, (List)into, (Predicate) predicate); + } + } + + public boolean getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate, final int maxCount) { + final EntityCollectionBySection byType = this.entitiesByType.get(type); + + if (byType != null) { + return byType.getEntitiesLimited((Entity)null, box, (List)into, (Predicate)predicate, maxCount); + } + + return false; + } + + protected EntityCollectionBySection initClass(final Class clazz) { + final EntityCollectionBySection ret = new EntityCollectionBySection(this); + + for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) { + final BasicEntityList sectionEntities = this.allEntities.entitiesBySection[sectionIndex]; + if (sectionEntities == null) { + continue; + } + + final Entity[] storage = sectionEntities.storage; + + for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (clazz.isInstance(entity)) { + ret.addEntity(entity, sectionIndex); + } + } + } + + return ret; + } + + public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, + final Predicate predicate) { + EntityCollectionBySection collection = this.entitiesByClass.get(clazz); + if (collection != null) { + collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); + } else { + this.entitiesByClass.put(clazz, collection = this.initClass(clazz)); + collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); + } + } + + public boolean getEntities(final Class clazz, final Entity except, final AABB box, final List into, + final Predicate predicate, final int maxCount) { + EntityCollectionBySection collection = this.entitiesByClass.get(clazz); + if (collection != null) { + return collection.getEntitiesWithEnderDragonPartsLimited(except, clazz, box, (List)into, (Predicate)predicate, maxCount); + } else { + this.entitiesByClass.put(clazz, collection = this.initClass(clazz)); + return collection.getEntitiesWithEnderDragonPartsLimited(except, clazz, box, (List)into, (Predicate)predicate, maxCount); + } + } + + protected static final class BasicEntityList { + + protected static final Entity[] EMPTY = new Entity[0]; + protected static final int DEFAULT_CAPACITY = 4; + + protected E[] storage; + protected int size; + + public BasicEntityList() { + this(0); + } + + public BasicEntityList(final int cap) { + this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]); + } + + public boolean isEmpty() { + return this.size == 0; + } + + public int size() { + return this.size; + } + + private void resize() { + if (this.storage == EMPTY) { + this.storage = (E[])new Entity[DEFAULT_CAPACITY]; + } else { + this.storage = Arrays.copyOf(this.storage, this.storage.length * 2); + } + } + + public void add(final E entity) { + final int idx = this.size++; + if (idx >= this.storage.length) { + this.resize(); + this.storage[idx] = entity; + } else { + this.storage[idx] = entity; + } + } + + public int indexOf(final E entity) { + final E[] storage = this.storage; + + for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) { + if (storage[i] == entity) { + return i; + } + } + + return -1; + } + + public boolean remove(final E entity) { + final int idx = this.indexOf(entity); + if (idx == -1) { + return false; + } + + final int size = --this.size; + final E[] storage = this.storage; + if (idx != size) { + System.arraycopy(storage, idx + 1, storage, idx, size - idx); + } + + storage[size] = null; + + return true; + } + + public boolean has(final E entity) { + return this.indexOf(entity) != -1; + } + } + + protected static final class EntityCollectionBySection { + + protected final ChunkEntitySlices manager; + protected final long[] nonEmptyBitset; + protected final BasicEntityList[] entitiesBySection; + protected int count; + + public EntityCollectionBySection(final ChunkEntitySlices manager) { + this.manager = manager; + + final int sectionCount = manager.maxSection - manager.minSection + 1; + + this.nonEmptyBitset = new long[(sectionCount + (Long.SIZE - 1)) >>> 6]; // (sectionCount + (Long.SIZE - 1)) / Long.SIZE + this.entitiesBySection = new BasicEntityList[sectionCount]; + } + + public void addEntity(final Entity entity, final int sectionIndex) { + BasicEntityList list = this.entitiesBySection[sectionIndex]; + + if (list != null && list.has(entity)) { + return; + } + + if (list == null) { + this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>(); + this.nonEmptyBitset[sectionIndex >>> 6] |= (1L << (sectionIndex & (Long.SIZE - 1))); + } + + list.add(entity); + ++this.count; + } + + public void removeEntity(final Entity entity, final int sectionIndex) { + final BasicEntityList list = this.entitiesBySection[sectionIndex]; + + if (list == null || !list.remove(entity)) { + return; + } + + --this.count; + + if (list.isEmpty()) { + this.entitiesBySection[sectionIndex] = null; + this.nonEmptyBitset[sectionIndex >>> 6] ^= (1L << (sectionIndex & (Long.SIZE - 1))); + } + } + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + if (this.count == 0) { + return; + } + + final int minSection = this.manager.minSection; + final int maxSection = this.manager.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate != null && !predicate.test(entity)) { + continue; + } + + into.add(entity); + } + } + } + + public boolean getEntitiesLimited(final Entity except, final AABB box, final List into, final Predicate predicate, + final int maxCount) { + if (this.count == 0) { + return false; + } + + final int minSection = this.manager.minSection; + final int maxSection = this.manager.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate != null && !predicate.test(entity)) { + continue; + } + + into.add(entity); + if (into.size() >= maxCount) { + return true; + } + } + } + + return false; + } + + public void getEntitiesWithEnderDragonParts(final Entity except, final AABB box, final List into, + final Predicate predicate) { + if (this.count == 0) { + return; + } + + final int minSection = this.manager.minSection; + final int maxSection = this.manager.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate == null || predicate.test(entity)) { + into.add(entity); + } // else: continue to test the ender dragon parts + + if (entity instanceof EnderDragon) { + for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) { + if (part == except || !part.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate != null && !predicate.test(part)) { + continue; + } + + into.add(part); + } + } + } + } + } + + public boolean getEntitiesWithEnderDragonPartsLimited(final Entity except, final AABB box, final List into, + final Predicate predicate, final int maxCount) { + if (this.count == 0) { + return false; + } + + final int minSection = this.manager.minSection; + final int maxSection = this.manager.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate == null || predicate.test(entity)) { + into.add(entity); + if (into.size() >= maxCount) { + return true; + } + } // else: continue to test the ender dragon parts + + if (entity instanceof EnderDragon) { + for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) { + if (part == except || !part.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate != null && !predicate.test(part)) { + continue; + } + + into.add(part); + if (into.size() >= maxCount) { + return true; + } + } + } + } + } + + return false; + } + + public void getEntitiesWithEnderDragonParts(final Entity except, final Class clazz, final AABB box, final List into, + final Predicate predicate) { + if (this.count == 0) { + return; + } + + final int minSection = this.manager.minSection; + final int maxSection = this.manager.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate == null || predicate.test(entity)) { + into.add(entity); + } // else: continue to test the ender dragon parts + + if (entity instanceof EnderDragon) { + for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) { + if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) { + continue; + } + + if (predicate != null && !predicate.test(part)) { + continue; + } + + into.add(part); + } + } + } + } + } + + public boolean getEntitiesWithEnderDragonPartsLimited(final Entity except, final Class clazz, final AABB box, final List into, + final Predicate predicate, final int maxCount) { + if (this.count == 0) { + return false; + } + + final int minSection = this.manager.minSection; + final int maxSection = this.manager.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate == null || predicate.test(entity)) { + into.add(entity); + if (into.size() >= maxCount) { + return true; + } + } // else: continue to test the ender dragon parts + + if (entity instanceof EnderDragon) { + for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) { + if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) { + continue; + } + + if (predicate != null && !predicate.test(part)) { + continue; + } + + into.add(part); + if (into.size() >= maxCount) { + return true; + } + } + } + } + } + + return false; + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/slices/EntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/slices/EntityLookup.java new file mode 100644 index 0000000..19db5d8 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/slices/EntityLookup.java @@ -0,0 +1,545 @@ +package ca.spottedleaf.moonrise.patches.collisions.slices; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import com.mojang.logging.LogUtils; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import org.slf4j.Logger; + +import java.util.List; +import java.util.function.Predicate; + +public final class EntityLookup { + + private static final Logger LOGGER = LogUtils.getLogger(); + + protected static final int REGION_SHIFT = 5; + protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1; + protected static final int REGION_SIZE = 1 << REGION_SHIFT; + + public final Level world; + protected final Long2ObjectOpenHashMap regions = new Long2ObjectOpenHashMap<>(128, 0.5f); + + public EntityLookup(final Level world) { + this.world = world; + } + + public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List into, final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.sectionVisibility.isAccessible()) { + continue; + } + + chunk.getEntitiesWithoutDragonParts(except, box, into, predicate); + } + } + } + } + } + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.sectionVisibility.isAccessible()) { + continue; + } + + chunk.getEntities(except, box, into, predicate); + } + } + } + } + } + + public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.sectionVisibility.isAccessible()) { + continue; + } + + chunk.getHardCollidingEntities(except, box, into, predicate); + } + } + } + } + } + + public void getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.sectionVisibility.isAccessible()) { + continue; + } + + chunk.getEntities(type, box, (List)into, (Predicate)predicate); + } + } + } + } + } + + public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, + final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.sectionVisibility.isAccessible()) { + continue; + } + + chunk.getEntities(clazz, except, box, into, predicate); + } + } + } + } + } + + //////// Limited //////// + + public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List into, final Predicate predicate, + final int maxCount) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.sectionVisibility.isAccessible()) { + continue; + } + + if (chunk.getEntitiesWithoutDragonParts(except, box, into, predicate, maxCount)) { + return; + } + } + } + } + } + } + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate, + final int maxCount) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.sectionVisibility.isAccessible()) { + continue; + } + + if (chunk.getEntities(except, box, into, predicate, maxCount)) { + return; + } + } + } + } + } + } + + public void getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate, final int maxCount) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.sectionVisibility.isAccessible()) { + continue; + } + + if (chunk.getEntities(type, box, (List)into, (Predicate)predicate, maxCount)) { + return; + } + } + } + } + } + } + + public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, + final Predicate predicate, final int maxCount) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.sectionVisibility.isAccessible()) { + continue; + } + + if (chunk.getEntities(clazz, except, box, into, predicate, maxCount)) { + return; + } + } + } + } + } + } + + public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) { + final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + if (region == null) { + return null; + } + + return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT)); + } + + public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) { + final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + final int localIdx = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); + + ChunkEntitySlices ret; + if (region != null && (ret = region.get(localIdx)) != null) { + return ret; + } + + ret = new ChunkEntitySlices(this.world, chunkX, chunkZ); + this.addChunk(chunkX, chunkZ, ret); + return ret; + } + + public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) { + return this.regions.get(CoordinateUtils.getChunkKey(regionX, regionZ)); + } + + private void removeChunk(final int chunkX, final int chunkZ) { + final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); + + final ChunkSlicesRegion region = this.regions.get(key); + final int remaining = region.remove(relIndex); + + if (remaining == 0) { + this.regions.remove(key); + } + } + + private void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { + final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); + + ChunkSlicesRegion region = this.regions.get(key); + if (region != null) { + region.add(relIndex, slices); + } else { + region = new ChunkSlicesRegion(); + region.add(relIndex, slices); + this.regions.put(key, region); + } + } + + public void addEntity(final Entity entity) { + final BlockPos pos = entity.blockPosition(); + final int sectionX = pos.getX() >> 4; + final int sectionY = Mth.clamp(pos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); + final int sectionZ = pos.getZ() >> 4; + + if (entity.isRemoved()) { + LOGGER.warn("Refusing to add removed entity: " + entity); + return; + } + + final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ); + if (!slices.addEntity(entity, sectionY)) { + LOGGER.warn("Entity " + entity + " added to world, but was already contained in entity chunk (" + sectionX + "," + sectionZ + ")"); + } + + return; + } + + public void moveEntity(final Entity entity, final long oldSection) { + final int minSection = WorldUtil.getMinSection(this.world); + final int maxSection = WorldUtil.getMaxSection(this.world); + + final int oldSectionX = CoordinateUtils.getChunkSectionX(oldSection); + final int oldSectionY = Mth.clamp(CoordinateUtils.getChunkSectionY(oldSection), minSection, maxSection); + final int oldSectionZ = CoordinateUtils.getChunkSectionZ(oldSection); + + final BlockPos newPos = entity.blockPosition(); + final int newSectionX = newPos.getX() >> 4; + final int newSectionY = Mth.clamp(newPos.getY() >> 4, minSection, maxSection); + final int newSectionZ = newPos.getZ() >> 4; + + final ChunkEntitySlices old = this.getChunk(oldSectionX, oldSectionZ); + final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ); + + if (!old.removeEntity(entity, oldSectionY)) { + LOGGER.warn("Could not remove entity " + entity + " from its old chunk section (" + oldSectionX + "," + oldSectionY + "," + oldSectionZ + ") since it was not contained in the section"); + } + + if (!slices.addEntity(entity, newSectionY)) { + LOGGER.warn("Could not add entity " + entity + " to its new chunk section (" + newSectionX + "," + newSectionY + "," + newSectionZ + ") as it is already contained in the section"); + } + } + + public void removeEntity(final Entity entity) { + final BlockPos newPos = entity.blockPosition(); + final int sectionX = newPos.getX() >> 4; + final int sectionY = Mth.clamp(newPos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); + final int sectionZ = newPos.getZ() >> 4; + + final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ); + // all entities should be in a chunk + if (slices == null) { + LOGGER.warn("Cannot remove entity " + entity + " from null entity slices (" + sectionX + "," + sectionZ + ")"); + } else { + if (!slices.removeEntity(entity, sectionY)) { + LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")"); + } + } + } + + public static final class ChunkSlicesRegion { + + protected final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE]; + protected int sliceCount; + + public ChunkEntitySlices get(final int index) { + return this.slices[index]; + } + + public int remove(final int index) { + final ChunkEntitySlices slices = this.slices[index]; + if (slices == null) { + throw new IllegalStateException(); + } + + this.slices[index] = null; + + return --this.sliceCount; + } + + public void add(final int index, final ChunkEntitySlices slices) { + final ChunkEntitySlices curr = this.slices[index]; + if (curr != null) { + throw new IllegalStateException(); + } + + this.slices[index] = slices; + + ++this.sliceCount; + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java new file mode 100644 index 0000000..875b9f3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.collisions.util; + +public interface CollisionDirection { + + // note: this is HashCommon#murmurHash3(some unique id) and since murmurHash3 has an inverse function the returned + // value is still unique + public int uniqueId(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java new file mode 100644 index 0000000..c250bde --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java @@ -0,0 +1,242 @@ +package ca.spottedleaf.moonrise.patches.collisions.util; + +import org.jetbrains.annotations.NotNull; +import java.util.Comparator; +import java.util.Iterator; +import java.util.Optional; +import java.util.Spliterator; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BinaryOperator; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; +import java.util.stream.Collector; +import java.util.stream.DoubleStream; +import java.util.stream.IntStream; +import java.util.stream.LongStream; +import java.util.stream.Stream; + +public final class EmptyStreamForMoveCall implements Stream { + + public static final EmptyStreamForMoveCall INSTANCE = new EmptyStreamForMoveCall(); + + @Override + public boolean noneMatch(Predicate predicate) { + return false; // important: ret false so the branch is never taken by mojang code + } + + @Override + public Stream filter(Predicate predicate) { + return null; + } + + @Override + public Stream map(Function mapper) { + return null; + } + + @Override + public IntStream mapToInt(ToIntFunction mapper) { + return null; + } + + @Override + public LongStream mapToLong(ToLongFunction mapper) { + return null; + } + + @Override + public DoubleStream mapToDouble(ToDoubleFunction mapper) { + return null; + } + + @Override + public Stream flatMap(Function> mapper) { + return null; + } + + @Override + public IntStream flatMapToInt(Function mapper) { + return null; + } + + @Override + public LongStream flatMapToLong(Function mapper) { + return null; + } + + @Override + public DoubleStream flatMapToDouble(Function mapper) { + return null; + } + + @Override + public Stream distinct() { + return null; + } + + @Override + public Stream sorted() { + return null; + } + + @Override + public Stream sorted(Comparator comparator) { + return null; + } + + @Override + public Stream peek(Consumer action) { + return null; + } + + @Override + public Stream limit(long maxSize) { + return null; + } + + @Override + public Stream skip(long n) { + return null; + } + + @Override + public void forEach(Consumer action) { + + } + + @Override + public void forEachOrdered(Consumer action) { + + } + + @NotNull + @Override + public Object[] toArray() { + return new Object[0]; + } + + @NotNull + @Override + public A[] toArray(IntFunction generator) { + return null; + } + + @Override + public T reduce(T identity, BinaryOperator accumulator) { + return null; + } + + @NotNull + @Override + public Optional reduce(BinaryOperator accumulator) { + return Optional.empty(); + } + + @Override + public U reduce(U identity, BiFunction accumulator, BinaryOperator combiner) { + return null; + } + + @Override + public R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner) { + return null; + } + + @Override + public R collect(Collector collector) { + return null; + } + + @NotNull + @Override + public Optional min(Comparator comparator) { + return Optional.empty(); + } + + @NotNull + @Override + public Optional max(Comparator comparator) { + return Optional.empty(); + } + + @Override + public long count() { + return 0; + } + + @Override + public boolean anyMatch(Predicate predicate) { + return false; + } + + @Override + public boolean allMatch(Predicate predicate) { + return false; + } + + @NotNull + @Override + public Optional findFirst() { + return Optional.empty(); + } + + @NotNull + @Override + public Optional findAny() { + return Optional.empty(); + } + + + @NotNull + @Override + public Iterator iterator() { + return null; + } + + @NotNull + @Override + public Spliterator spliterator() { + return null; + } + + @Override + public boolean isParallel() { + return false; + } + + @NotNull + @Override + public Stream sequential() { + return null; + } + + @NotNull + @Override + public Stream parallel() { + return null; + } + + @NotNull + @Override + public Stream unordered() { + return null; + } + + @NotNull + @Override + public Stream onClose(Runnable closeHandler) { + return null; + } + + @Override + public void close() { + + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java new file mode 100644 index 0000000..cf9ffde --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java @@ -0,0 +1,7 @@ +package ca.spottedleaf.moonrise.patches.collisions.util; + +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.state.BlockState; + +public record FluidOcclusionCacheKey(BlockState first, BlockState second, Direction direction, boolean result) { +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/BlockCounter.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/BlockCounter.java new file mode 100644 index 0000000..a8e6e61 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/BlockCounter.java @@ -0,0 +1,38 @@ +package ca.spottedleaf.moonrise.patches.collisions.world; + +import ca.spottedleaf.moonrise.patches.collisions.CollisionUtil; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.material.FluidState; + +public final class BlockCounter implements PalettedContainer.CountConsumer { + public int nonEmptyBlockCount; + public int tickingBlockCount; + public int tickingFluidCount; + public int specialCollidingBlocks; + + @Override + public void accept(final BlockState state, final int count) { + // our logic + if (CollisionUtil.isSpecialCollidingBlock(state)) { + this.specialCollidingBlocks += count; + } + + // Vanilla logic + if (!state.isAir()) { + this.nonEmptyBlockCount += count; + if (state.isRandomlyTicking()) { + this.tickingBlockCount += count; + } + } + + final FluidState fluid = state.getFluidState(); + + if (!fluid.isEmpty()) { + //this.nonEmptyBlockCount += i; // fix vanilla bug: make non empty block count correct + if (fluid.isRandomlyTicking()) { + this.tickingFluidCount += count; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionEntityGetter.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionEntityGetter.java new file mode 100644 index 0000000..812cf0c --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionEntityGetter.java @@ -0,0 +1,12 @@ +package ca.spottedleaf.moonrise.patches.collisions.world; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import java.util.List; +import java.util.function.Predicate; + +public interface CollisionEntityGetter { + + public List getHardCollidingEntities(final Entity entity, final AABB box, final Predicate predicate); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java new file mode 100644 index 0000000..bc49c75 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java @@ -0,0 +1,15 @@ +package ca.spottedleaf.moonrise.patches.collisions.world; + +import ca.spottedleaf.moonrise.patches.collisions.slices.EntityLookup; + +public interface CollisionLevel { + + public EntityLookup getCollisionLookup(); + + // avoid name conflicts by appending mod name + + public int getMinSectionMoonrise(); + + public int getMaxSectionMoonrise(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevelChunkSection.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevelChunkSection.java new file mode 100644 index 0000000..09e79c0 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevelChunkSection.java @@ -0,0 +1,7 @@ +package ca.spottedleaf.moonrise.patches.collisions.world; + +public interface CollisionLevelChunkSection { + + public int getSpecialCollidingBlocks(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/explosions/ExplosionBlockCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/explosions/ExplosionBlockCache.java new file mode 100644 index 0000000..b235934 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/explosions/ExplosionBlockCache.java @@ -0,0 +1,28 @@ +package ca.spottedleaf.moonrise.patches.explosions; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.shapes.VoxelShape; + +public final class ExplosionBlockCache { + + public final long key; + public final BlockPos immutablePos; + public final BlockState blockState; + public final FluidState fluidState; + public final float resistance; + public final boolean outOfWorld; + public Boolean shouldExplode; // null -> not called yet + public VoxelShape cachedCollisionShape; + + public ExplosionBlockCache(long key, BlockPos immutablePos, BlockState blockState, FluidState fluidState, float resistance, + boolean outOfWorld) { + this.key = key; + this.immutablePos = immutablePos; + this.blockState = blockState; + this.fluidState = fluidState; + this.resistance = resistance; + this.outOfWorld = outOfWorld; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/fluids/FluidClassification.java b/src/main/java/ca/spottedleaf/moonrise/patches/fluids/FluidClassification.java new file mode 100644 index 0000000..f06701f --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/fluids/FluidClassification.java @@ -0,0 +1,7 @@ +package ca.spottedleaf.moonrise.patches.fluids; + +public enum FluidClassification { + EMPTY, + WATER, + LAVA; +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/fluids/FluidFluid.java b/src/main/java/ca/spottedleaf/moonrise/patches/fluids/FluidFluid.java new file mode 100644 index 0000000..6db2b79 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/fluids/FluidFluid.java @@ -0,0 +1,7 @@ +package ca.spottedleaf.moonrise.patches.fluids; + +public interface FluidFluid { + + public FluidClassification getClassification(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/fluids/FluidFluidState.java b/src/main/java/ca/spottedleaf/moonrise/patches/fluids/FluidFluidState.java new file mode 100644 index 0000000..273f3f3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/fluids/FluidFluidState.java @@ -0,0 +1,6 @@ +package ca.spottedleaf.moonrise.patches.fluids; + +public interface FluidFluidState { + + public FluidClassification getClassification(); +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/hopper/README.md b/src/main/java/ca/spottedleaf/moonrise/patches/hopper/README.md new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/poi_lookup/PoiAccess.java b/src/main/java/ca/spottedleaf/moonrise/patches/poi_lookup/PoiAccess.java new file mode 100644 index 0000000..665fbb0 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/poi_lookup/PoiAccess.java @@ -0,0 +1,806 @@ +package ca.spottedleaf.moonrise.patches.poi_lookup; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import com.mojang.datafixers.util.Pair; +import it.unimi.dsi.fastutil.doubles.Double2ObjectMap; +import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import java.util.function.BiPredicate; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Provides optimised access to POI data. All returned values will be identical to vanilla. + */ +public final class PoiAccess { + + protected static double clamp(final double val, final double min, final double max) { + return (val < min ? min : (val > max ? max : val)); + } + + protected static double getSmallestDistanceSquared(final double boxMinX, final double boxMinY, final double boxMinZ, + final double boxMaxX, final double boxMaxY, final double boxMaxZ, + + final double circleX, final double circleY, final double circleZ) { + // is the circle center inside the box? + if (circleX >= boxMinX && circleX <= boxMaxX && circleY >= boxMinY && circleY <= boxMaxY && circleZ >= boxMinZ && circleZ <= boxMaxZ) { + return 0.0; + } + + final double boxWidthX = (boxMaxX - boxMinX) / 2.0; + final double boxWidthY = (boxMaxY - boxMinY) / 2.0; + final double boxWidthZ = (boxMaxZ - boxMinZ) / 2.0; + + final double boxCenterX = (boxMinX + boxMaxX) / 2.0; + final double boxCenterY = (boxMinY + boxMaxY) / 2.0; + final double boxCenterZ = (boxMinZ + boxMaxZ) / 2.0; + + double centerDiffX = circleX - boxCenterX; + double centerDiffY = circleY - boxCenterY; + double centerDiffZ = circleZ - boxCenterZ; + + centerDiffX = circleX - (clamp(centerDiffX, -boxWidthX, boxWidthX) + boxCenterX); + centerDiffY = circleY - (clamp(centerDiffY, -boxWidthY, boxWidthY) + boxCenterY); + centerDiffZ = circleZ - (clamp(centerDiffZ, -boxWidthZ, boxWidthZ) + boxCenterZ); + + return (centerDiffX * centerDiffX) + (centerDiffY * centerDiffY) + (centerDiffZ * centerDiffZ); + } + + + // key is: + // upper 32 bits: + // upper 16 bits: max y section + // lower 16 bits: min y section + // lower 32 bits: + // upper 16 bits: section + // lower 16 bits: radius + protected static long getKey(final int minSection, final int maxSection, final int section, final int radius) { + return ( + (maxSection & 0xFFFFL) << (64 - 16) + | (minSection & 0xFFFFL) << (64 - 32) + | (section & 0xFFFFL) << (64 - 48) + | (radius & 0xFFFFL) << (64 - 64) + ); + } + + // only includes x/z axis + // finds the closest poi data by distance. + public static BlockPos findClosestPoiDataPosition(final PoiManager poiStorage, + final Predicate> villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistanceSquared, + final PoiManager.Occupancy occupancy, + final boolean load) { + final PoiRecord ret = findClosestPoiDataRecord( + poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load + ); + + return ret == null ? null : ret.getPos(); + } + + // only includes x/z axis + // finds the closest poi data by distance. + public static Pair, BlockPos> findClosestPoiDataTypeAndPosition(final PoiManager poiStorage, + final Predicate> villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistanceSquared, + final PoiManager.Occupancy occupancy, + final boolean load) { + final PoiRecord ret = findClosestPoiDataRecord( + poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load + ); + + return ret == null ? null : Pair.of(ret.getPoiType(), ret.getPos()); + } + + // only includes x/z axis + // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. + public static void findClosestPoiDataPositions(final PoiManager poiStorage, + final Predicate> villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistanceSquared, + final PoiManager.Occupancy occupancy, + final boolean load, + final Set ret) { + final Set positions = new HashSet<>(); + // pos predicate is last thing that runs before adding to ret. + final Predicate newPredicate = (final BlockPos pos) -> { + if (positionPredicate != null && !positionPredicate.test(pos)) { + return false; + } + return positions.add(pos.immutable()); + }; + + final List toConvert = new ArrayList<>(); + findClosestPoiDataRecords( + poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, toConvert + ); + + for (final PoiRecord record : toConvert) { + ret.add(record.getPos()); + } + } + + // only includes x/z axis + // finds the closest poi data by distance. + public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage, + final Predicate> villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistanceSquared, + final PoiManager.Occupancy occupancy, + final boolean load) { + final List ret = new ArrayList<>(); + findClosestPoiDataRecords( + poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret + ); + return ret.isEmpty() ? null : ret.get(0); + } + + // only includes x/z axis + // finds the closest poi data by distance. + public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage, + final Predicate> villagePlaceType, + // position predicate must not modify chunk POI + final BiPredicate, BlockPos> predicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistanceSquared, + final PoiManager.Occupancy occupancy, + final boolean load) { + final List ret = new ArrayList<>(); + findClosestPoiDataRecords( + poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret + ); + return ret.isEmpty() ? null : ret.get(0); + } + + // only includes x/z axis + // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. + public static void findClosestPoiDataRecords(final PoiManager poiStorage, + final Predicate> villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistanceSquared, + final PoiManager.Occupancy occupancy, + final boolean load, + final List ret) { + final BiPredicate, BlockPos> predicate = positionPredicate != null ? (type, pos) -> positionPredicate.test(pos) : null; + findClosestPoiDataRecords(poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret); + } + + public static void findClosestPoiDataRecords(final PoiManager poiStorage, + final Predicate> villagePlaceType, + // position predicate must not modify chunk POI + final BiPredicate, BlockPos> predicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistanceSquared, + final PoiManager.Occupancy occupancy, + final boolean load, + final List ret) { + final Predicate occupancyFilter = occupancy.getTest(); + + final List closestRecords = new ArrayList<>(); + double closestDistanceSquared = maxDistanceSquared; + + final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; + final int lowerY = WorldUtil.getMinSection(poiStorage.levelHeightAccessor); + final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; + final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; + final int upperY = WorldUtil.getMaxSection(poiStorage.levelHeightAccessor); + final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; + + final int centerX = sourcePosition.getX() >> 4; + final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); + final int centerZ = sourcePosition.getZ() >> 4; + final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ); + + final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); + final LongOpenHashSet seen = new LongOpenHashSet(); + seen.add(centerKey); + queue.enqueue(centerKey); + + while (!queue.isEmpty()) { + final long key = queue.dequeueLong(); + final int sectionX = CoordinateUtils.getChunkSectionX(key); + final int sectionY = CoordinateUtils.getChunkSectionY(key); + final int sectionZ = CoordinateUtils.getChunkSectionZ(key); + + if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { + // out of bound chunk + continue; + } + + final double sectionDistanceSquared = getSmallestDistanceSquared( + (double)(sectionX << 4), + (double)(sectionY << 4), + (double)(sectionZ << 4), + (double)((sectionX << 4) | 15), + (double)((sectionY << 4) | 15), + (double)((sectionZ << 4) | 15), + (double)sourcePosition.getX(), (double)sourcePosition.getY(), (double)sourcePosition.getZ() + ); + if (sectionDistanceSquared > closestDistanceSquared) { + continue; + } + + // queue all neighbours + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + for (int dy = -1; dy <= 1; ++dy) { + // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many + // values are set. we only care about cardinal neighbours, so, we only care if one value is set + if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { + continue; + } + + final int neighbourX = sectionX + dx; + final int neighbourY = sectionY + dy; + final int neighbourZ = sectionZ + dz; + + final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); + if (seen.add(neighbourKey)) { + queue.enqueue(neighbourKey); + } + } + } + } + + final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); + + if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { + continue; + } + + final PoiSection poiSection = poiSectionOptional.get(); + + final Map, Set> sectionData = poiSection.byType; + if (sectionData.isEmpty()) { + continue; + } + + // now we search the section data + for (final Map.Entry, Set> entry : sectionData.entrySet()) { + if (!villagePlaceType.test(entry.getKey())) { + // filter out by poi type + continue; + } + + // now we can look at the poi data + for (final PoiRecord poiData : entry.getValue()) { + if (!occupancyFilter.test(poiData)) { + // filter by occupancy + continue; + } + + final BlockPos poiPosition = poiData.getPos(); + + if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range + || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { + // out of range for square radius + continue; + } + + // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! + final double dataRange = poiPosition.distSqr(sourcePosition); + + if (dataRange > closestDistanceSquared) { + // out of range for distance check + continue; + } + + if (predicate != null && !predicate.test(poiData.getPoiType(), poiPosition)) { + // filter by position + continue; + } + + if (dataRange < closestDistanceSquared) { + closestRecords.clear(); + closestDistanceSquared = dataRange; + } + closestRecords.add(poiData); + } + } + } + + // uh oh! we might have multiple records that match the distance sorting! + // we need to re-order our results by the way vanilla would have iterated over them. + closestRecords.sort((record1, record2) -> { + // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section + // is fine and should be preserved (this sort is stable so we're good there) + // but they iterate sections by x then by z (like the following) + // for (int x = -dx; x <= dx; ++x) + // for (int z = -dz; z <= dz; ++z) + // .... + // so we need to reorder such that records with lower chunk z, then lower chunk x come first + final BlockPos pos1 = record1.getPos(); + final BlockPos pos2 = record2.getPos(); + + final int cx1 = pos1.getX() >> 4; + final int cz1 = pos1.getZ() >> 4; + + final int cx2 = pos2.getX() >> 4; + final int cz2 = pos2.getZ() >> 4; + + if (cz2 != cz1) { + // want smaller z + return Integer.compare(cz1, cz2); + } + + if (cx2 != cx1) { + // want smaller x + return Integer.compare(cx1, cx2); + } + + // same chunk + // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y + // so now we just compare section y, wanting smaller y + + return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); + }); + + // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). + ret.addAll(closestRecords); + } + + // finds the closest poi entry pos. + public static BlockPos findNearestPoiPosition(final PoiManager poiStorage, + final Predicate> villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistanceSquared, + final PoiManager.Occupancy occupancy, + final boolean load) { + final PoiRecord ret = findNearestPoiRecord( + poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load + ); + return ret == null ? null : ret.getPos(); + } + + // finds the closest `max` poi entry positions. + public static void findNearestPoiPositions(final PoiManager poiStorage, + final Predicate> villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistanceSquared, + final PoiManager.Occupancy occupancy, + final boolean load, + final int max, + final List, BlockPos>> ret) { + final Set positions = new HashSet<>(); + // pos predicate is last thing that runs before adding to ret. + final Predicate newPredicate = (final BlockPos pos) -> { + if (positionPredicate != null && !positionPredicate.test(pos)) { + return false; + } + return positions.add(pos.immutable()); + }; + + final List toConvert = new ArrayList<>(); + findNearestPoiRecords( + poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, max, toConvert + ); + + for (final PoiRecord record : toConvert) { + ret.add(Pair.of(record.getPoiType(), record.getPos())); + } + } + + // finds the closest poi entry. + public static PoiRecord findNearestPoiRecord(final PoiManager poiStorage, + final Predicate> villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistanceSquared, + final PoiManager.Occupancy occupancy, + final boolean load) { + final List ret = new ArrayList<>(); + findNearestPoiRecords( + poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, + 1, ret + ); + return ret.isEmpty() ? null : ret.get(0); + } + + // finds the closest `max` poi entries. + public static void findNearestPoiRecords(final PoiManager poiStorage, + final Predicate> villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistanceSquared, + final PoiManager.Occupancy occupancy, + final boolean load, + final int max, + final List ret) { + final Predicate occupancyFilter = occupancy.getTest(); + + final Double2ObjectRBTreeMap> closestRecords = new Double2ObjectRBTreeMap<>(); + int totalRecords = 0; + double furthestDistanceSquared = maxDistanceSquared; + + final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; + final int lowerY = WorldUtil.getMinSection(poiStorage.levelHeightAccessor); + final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; + final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; + final int upperY = WorldUtil.getMaxSection(poiStorage.levelHeightAccessor); + final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; + + final int centerX = sourcePosition.getX() >> 4; + final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); + final int centerZ = sourcePosition.getZ() >> 4; + final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ); + + final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); + final LongOpenHashSet seen = new LongOpenHashSet(); + seen.add(centerKey); + queue.enqueue(centerKey); + + while (!queue.isEmpty()) { + final long key = queue.dequeueLong(); + final int sectionX = CoordinateUtils.getChunkSectionX(key); + final int sectionY = CoordinateUtils.getChunkSectionY(key); + final int sectionZ = CoordinateUtils.getChunkSectionZ(key); + + if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { + // out of bound chunk + continue; + } + + final double sectionDistanceSquared = getSmallestDistanceSquared( + (double)(sectionX << 4), + (double)(sectionY << 4), + (double)(sectionZ << 4), + (double)((sectionX << 4) | 15), + (double)((sectionY << 4) | 15), + (double)((sectionZ << 4) | 15), + (double)sourcePosition.getX(), (double)sourcePosition.getY(), (double)sourcePosition.getZ() + ); + + if (sectionDistanceSquared > (totalRecords >= max ? furthestDistanceSquared : maxDistanceSquared)) { + continue; + } + + // queue all neighbours + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + for (int dy = -1; dy <= 1; ++dy) { + // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many + // values are set. we only care about cardinal neighbours, so, we only care if one value is set + if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { + continue; + } + + final int neighbourX = sectionX + dx; + final int neighbourY = sectionY + dy; + final int neighbourZ = sectionZ + dz; + + final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); + if (seen.add(neighbourKey)) { + queue.enqueue(neighbourKey); + } + } + } + } + + final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); + + if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { + continue; + } + + final PoiSection poiSection = poiSectionOptional.get(); + + final Map, Set> sectionData = poiSection.byType; + if (sectionData.isEmpty()) { + continue; + } + + // now we search the section data + for (final Map.Entry, Set> entry : sectionData.entrySet()) { + if (!villagePlaceType.test(entry.getKey())) { + // filter out by poi type + continue; + } + + // now we can look at the poi data + for (final PoiRecord poiData : entry.getValue()) { + if (!occupancyFilter.test(poiData)) { + // filter by occupancy + continue; + } + + final BlockPos poiPosition = poiData.getPos(); + + if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range + || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { + // out of range for square radius + continue; + } + + // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! + final double dataRange = poiPosition.distSqr(sourcePosition); + + if (dataRange > maxDistanceSquared) { + // out of range for distance check + continue; + } + + if (dataRange > furthestDistanceSquared && totalRecords >= max) { + // out of range for distance check + continue; + } + + if (positionPredicate != null && !positionPredicate.test(poiPosition)) { + // filter by position + continue; + } + + if (dataRange > furthestDistanceSquared) { + // we know totalRecords < max, so this entry is now our furthest + furthestDistanceSquared = dataRange; + } + + closestRecords.computeIfAbsent(dataRange, (final double unused) -> { + return new ArrayList<>(); + }).add(poiData); + + if (++totalRecords >= max) { + if (closestRecords.size() >= 2) { + int entriesInClosest = 0; + final Iterator>> iterator = closestRecords.double2ObjectEntrySet().iterator(); + double nextFurthestDistanceSquared = 0.0; + + for (int i = 0, len = closestRecords.size() - 1; i < len; ++i) { + final Double2ObjectMap.Entry> recordEntry = iterator.next(); + entriesInClosest += recordEntry.getValue().size(); + nextFurthestDistanceSquared = recordEntry.getDoubleKey(); + } + + if (entriesInClosest >= max) { + // the last set of entries at range wont even be considered for sure... nuke em + final Double2ObjectMap.Entry> recordEntry = iterator.next(); + totalRecords -= recordEntry.getValue().size(); + iterator.remove(); + + furthestDistanceSquared = nextFurthestDistanceSquared; + } + } + } + } + } + } + + final List closestRecordsUnsorted = new ArrayList<>(); + + // we're done here, so now just flatten the map and sort it. + + for (final List records : closestRecords.values()) { + closestRecordsUnsorted.addAll(records); + } + + // uh oh! we might have multiple records that match the distance sorting! + // we need to re-order our results by the way vanilla would have iterated over them. + closestRecordsUnsorted.sort((record1, record2) -> { + // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section + // is fine and should be preserved (this sort is stable so we're good there) + // but they iterate sections by x then by z (like the following) + // for (int x = -dx; x <= dx; ++x) + // for (int z = -dz; z <= dz; ++z) + // .... + // so we need to reorder such that records with lower chunk z, then lower chunk x come first + final BlockPos pos1 = record1.getPos(); + final BlockPos pos2 = record2.getPos(); + + final int cx1 = pos1.getX() >> 4; + final int cz1 = pos1.getZ() >> 4; + + final int cx2 = pos2.getX() >> 4; + final int cz2 = pos2.getZ() >> 4; + + if (cz2 != cz1) { + // want smaller z + return Integer.compare(cz1, cz2); + } + + if (cx2 != cx1) { + // want smaller x + return Integer.compare(cx1, cx2); + } + + // same chunk + // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y + // so now we just compare section y, wanting smaller section y + + return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); + }); + + // trim out any entries exceeding our maximum + for (int i = closestRecordsUnsorted.size() - 1; i >= max; --i) { + closestRecordsUnsorted.remove(i); + } + + // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). + ret.addAll(closestRecordsUnsorted); + } + + public static BlockPos findAnyPoiPosition(final PoiManager poiStorage, + final Predicate> villagePlaceType, + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final PoiManager.Occupancy occupancy, + final boolean load) { + final PoiRecord ret = findAnyPoiRecord( + poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load + ); + + return ret == null ? null : ret.getPos(); + } + + public static void findAnyPoiPositions(final PoiManager poiStorage, + final Predicate> villagePlaceType, + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final PoiManager.Occupancy occupancy, + final boolean load, + final int max, + final List, BlockPos>> ret) { + final Set positions = new HashSet<>(); + // pos predicate is last thing that runs before adding to ret. + final Predicate newPredicate = (final BlockPos pos) -> { + if (positionPredicate != null && !positionPredicate.test(pos)) { + return false; + } + return positions.add(pos.immutable()); + }; + + final List toConvert = new ArrayList<>(); + findAnyPoiRecords( + poiStorage, villagePlaceType, newPredicate, sourcePosition, range, occupancy, load, max, toConvert + ); + + for (final PoiRecord record : toConvert) { + ret.add(Pair.of(record.getPoiType(), record.getPos())); + } + } + + public static PoiRecord findAnyPoiRecord(final PoiManager poiStorage, + final Predicate> villagePlaceType, + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final PoiManager.Occupancy occupancy, + final boolean load) { + final List ret = new ArrayList<>(); + findAnyPoiRecords(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load, 1, ret); + return ret.isEmpty() ? null : ret.get(0); + } + + public static void findAnyPoiRecords(final PoiManager poiStorage, + final Predicate> villagePlaceType, + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final PoiManager.Occupancy occupancy, + final boolean load, + final int max, + final List ret) { + // the biggest issue with the original mojang implementation is that they chain so many streams together + // the amount of streams chained just rolls performance, even if nothing is iterated over + final Predicate occupancyFilter = occupancy.getTest(); + final double rangeSquared = range * range; + + int added = 0; + + // First up, we need to iterate the chunks + // all the values here are in chunk sections + final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; + final int lowerY = Math.max(WorldUtil.getMinSection(poiStorage.levelHeightAccessor), Mth.floor(sourcePosition.getY() - range) >> 4); + final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; + final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; + final int upperY = Math.min(WorldUtil.getMaxSection(poiStorage.levelHeightAccessor), Mth.floor(sourcePosition.getY() + range) >> 4); + final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; + + // Vanilla iterates by x until max is reached then increases z + // vanilla also searches by increasing Y section value + for (int currZ = lowerZ; currZ <= upperZ; ++currZ) { + for (int currX = lowerX; currX <= upperX; ++currX) { + for (int currY = lowerY; currY <= upperY; ++currY) { // vanilla searches the entire chunk because they're actually stupid. just search the sections we need + final Optional poiSectionOptional = load ? poiStorage.getOrLoad(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)) : + poiStorage.get(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)); + final PoiSection poiSection = poiSectionOptional == null ? null : poiSectionOptional.orElse(null); + if (poiSection == null) { + continue; + } + + final Map, Set> sectionData = poiSection.byType; + if (sectionData.isEmpty()) { + continue; + } + + // now we search the section data + for (final Map.Entry, Set> entry : sectionData.entrySet()) { + if (!villagePlaceType.test(entry.getKey())) { + // filter out by poi type + continue; + } + + // now we can look at the poi data + for (final PoiRecord poiData : entry.getValue()) { + if (!occupancyFilter.test(poiData)) { + // filter by occupancy + continue; + } + + final BlockPos poiPosition = poiData.getPos(); + + if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range + || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { + // out of range for square radius + continue; + } + + if (poiPosition.distSqr(sourcePosition) > rangeSquared) { + // out of range for distance check + continue; + } + + if (positionPredicate != null && !positionPredicate.test(poiPosition)) { + // filter by position + continue; + } + + // found one! + ret.add(poiData); + if (++added >= max) { + return; + } + } + } + } + } + } + } + + private PoiAccess() { + throw new RuntimeException(); + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java new file mode 100644 index 0000000..5c139c2 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.starlight.blockstate; + +public interface StarlightAbstractBlockState { + + public boolean isConditionallyFullOpaque(); + + public int getOpacityIfCached(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java new file mode 100644 index 0000000..8221576 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java @@ -0,0 +1,18 @@ +package ca.spottedleaf.moonrise.patches.starlight.chunk; + +import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray; + +public interface StarlightChunk { + + public SWMRNibbleArray[] getBlockNibbles(); + public void setBlockNibbles(final SWMRNibbleArray[] nibbles); + + public SWMRNibbleArray[] getSkyNibbles(); + public void setSkyNibbles(final SWMRNibbleArray[] nibbles); + + public boolean[] getSkyEmptinessMap(); + public void setSkyEmptinessMap(final boolean[] emptinessMap); + + public boolean[] getBlockEmptinessMap(); + public void setBlockEmptinessMap(final boolean[] emptinessMap); +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java new file mode 100644 index 0000000..b4619e8 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java @@ -0,0 +1,277 @@ +package ca.spottedleaf.moonrise.patches.starlight.light; + +import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState; +import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public final class BlockStarLightEngine extends StarLightEngine { + + public BlockStarLightEngine(final Level world) { + super(false, world); + } + + @Override + protected boolean[] getEmptinessMap(final ChunkAccess chunk) { + return ((StarlightChunk)chunk).getBlockEmptinessMap(); + } + + @Override + protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { + ((StarlightChunk)chunk).setBlockEmptinessMap(to); + } + + @Override + protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { + return ((StarlightChunk)chunk).getBlockNibbles(); + } + + @Override + protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { + ((StarlightChunk)chunk).setBlockNibbles(to); + } + + @Override + protected boolean canUseChunk(final ChunkAccess chunk) { + return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); + } + + @Override + protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble != null) { + // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically + // because a block was removed - which can decrease light. with sky data, block breaking can only result + // in increases, and thus the existing sky block check will actually correctly propagate light through + // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove + // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running + // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence + // of vanilla data management we "hide" them. + nibble.setHidden(); + } + } + + @Override + protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) { + if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) { + return; + } + + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble == null) { + if (!initRemovedNibbles) { + throw new IllegalStateException(); + } else { + this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray()); + } + } else { + nibble.setNonNull(); + } + } + + @Override + protected final void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) { + // blocks can change opacity + // blocks can change emitted light + // blocks can change direction of propagation + + final int encodeOffset = this.coordinateOffset; + final int emittedMask = this.emittedLightMask; + + final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); + final BlockState blockState = this.getBlockState(worldX, worldY, worldZ); + final int emittedLevel = blockState.getLightEmission() & emittedMask; + + this.setLightLevel(worldX, worldY, worldZ, emittedLevel); + // this accounts for change in emitted light that would cause an increase + if (emittedLevel != 0) { + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (emittedLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (((StarlightAbstractBlockState)blockState).isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) + ); + } + // this also accounts for a change in emitted light that would cause a decrease + // this also accounts for the change of direction of propagation (i.e old block was full transparent, new block is full opaque or vice versa) + // as it checks all neighbours (even if current level is 0) + this.appendToDecreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (currentLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + // always keep sided transparent false here, new block might be conditionally transparent which would + // prevent us from decreasing sources in the directions where the new block is opaque + // if it turns out we were wrong to de-propagate the source, the re-propagate logic WILL always + // catch that and fix it. + ); + // re-propagating neighbours (done by the decrease queue) will also account for opacity changes in this block + } + + protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); + protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); + + @Override + protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect) { + final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); + int level = centerState.getLightEmission() & 0xF; + + if (level >= (15 - 1) || level > expect) { + return level; + } + + final int sectionOffset = this.chunkSectionIndexOffset; + final BlockState conditionallyOpaqueState; + int opacity = ((StarlightAbstractBlockState)centerState).getOpacityIfCached(); + + if (opacity == -1) { + this.recalcCenterPos.set(worldX, worldY, worldZ); + opacity = centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos); + if (((StarlightAbstractBlockState)centerState).isConditionallyFullOpaque()) { + conditionallyOpaqueState = centerState; + } else { + conditionallyOpaqueState = null; + } + } else if (opacity >= 15) { + return level; + } else { + conditionallyOpaqueState = null; + } + opacity = Math.max(1, opacity); + + for (final AxisDirection direction : AXIS_DIRECTIONS) { + final int offX = worldX + direction.x; + final int offY = worldY + direction.y; + final int offZ = worldZ + direction.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + + final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8)); + + if ((neighbourLevel - 1) <= level) { + // don't need to test transparency, we know it wont affect the result. + continue; + } + + final BlockState neighbourState = this.getBlockState(offX, offY, offZ); + if (((StarlightAbstractBlockState)neighbourState).isConditionallyFullOpaque()) { + // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that + // we don't read the blockstate because most of the time this is false, so using the faster + // known transparency lookup results in a net win + this.recalcNeighbourPos.set(offX, offY, offZ); + final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); + final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); + if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { + // not allowed to propagate + continue; + } + } + + // passed transparency, + + final int calculated = neighbourLevel - opacity; + level = Math.max(calculated, level); + if (level > expect) { + return level; + } + } + + return level; + } + + @Override + protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) { + for (final BlockPos pos : positions) { + this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); + } + + this.performLightDecrease(lightAccess); + } + + protected List getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) { + final List sources = new ArrayList<>(); + + final int offX = chunk.getPos().x << 4; + final int offZ = chunk.getPos().z << 4; + + final LevelChunkSection[] sections = chunk.getSections(); + for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { + final LevelChunkSection section = sections[sectionY - this.minSection]; + if (section == null || section.hasOnlyAir()) { + // no sources in empty sections + continue; + } + if (!section.maybeHas((final BlockState state) -> { + return state.getLightEmission() > 0; + })) { + // no light sources in palette + continue; + } + final PalettedContainer states = section.states; + final int offY = sectionY << 4; + + for (int index = 0; index < (16 * 16 * 16); ++index) { + final BlockState state = states.get(index); + if (state.getLightEmission() <= 0) { + continue; + } + + // index = x | (z << 4) | (y << 8) + sources.add(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15))); + } + } + + return sources; + } + + @Override + public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { + // setup sources + final int emittedMask = this.emittedLightMask; + final List positions = this.getSources(lightAccess, chunk); + for (int i = 0, len = positions.size(); i < len; ++i) { + final BlockPos pos = positions.get(i); + final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ()); + final int emittedLight = blockState.getLightEmission() & emittedMask; + + if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) { + // some other source is brighter + continue; + } + + this.appendToIncreaseQueue( + ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (emittedLight & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (((StarlightAbstractBlockState)blockState).isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) + ); + + + // propagation wont set this for us + this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight); + } + + if (needsEdgeChecks) { + // not required to propagate here, but this will reduce the hit of the edge checks + this.performLightIncrease(lightAccess); + + // verify neighbour edges + this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); + } else { + this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection); + + this.performLightIncrease(lightAccess); + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java new file mode 100644 index 0000000..4ca68a9 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java @@ -0,0 +1,440 @@ +package ca.spottedleaf.moonrise.patches.starlight.light; + +import net.minecraft.world.level.chunk.DataLayer; +import java.util.ArrayDeque; +import java.util.Arrays; + +// SWMR -> Single Writer Multi Reader Nibble Array +public final class SWMRNibbleArray { + + /* + * Null nibble - nibble does not exist, and should not be written to. Just like vanilla - null + * nibbles are always 0 - and they are never written to directly. Only initialised/uninitialised + * nibbles can be written to. + * + * Uninitialised nibble - They are all 0, but the backing array isn't initialised. + * + * Initialised nibble - Has light data. + */ + + protected static final int INIT_STATE_NULL = 0; // null + protected static final int INIT_STATE_UNINIT = 1; // uninitialised + protected static final int INIT_STATE_INIT = 2; // initialised + protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL + + public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block + // this allows us to maintain only 1 byte array when we're not updating + static final ThreadLocal> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new); + + private static byte[] allocateBytes() { + final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst(); + if (inPool != null) { + return inPool; + } + + return new byte[ARRAY_SIZE]; + } + + private static void freeBytes(final byte[] bytes) { + WORKING_BYTES_POOL.get().addFirst(bytes); + } + + public static SWMRNibbleArray fromVanilla(final DataLayer nibble) { + if (nibble == null) { + return new SWMRNibbleArray(null, true); + } else if (nibble.isEmpty()) { + return new SWMRNibbleArray(); + } else { + return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later + } + } + + protected int stateUpdating; + protected volatile int stateVisible; + + protected byte[] storageUpdating; + protected boolean updatingDirty; // only returns whether storageUpdating is dirty + protected volatile byte[] storageVisible; + + public SWMRNibbleArray() { + this(null, false); // lazy init + } + + public SWMRNibbleArray(final byte[] bytes) { + this(bytes, false); + } + + public SWMRNibbleArray(final byte[] bytes, final boolean isNullNibble) { + if (bytes != null && bytes.length != ARRAY_SIZE) { + throw new IllegalArgumentException("Data of wrong length: " + bytes.length); + } + this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT; + this.storageUpdating = this.storageVisible = bytes; + } + + public SWMRNibbleArray(final byte[] bytes, final int state) { + if (bytes != null && bytes.length != ARRAY_SIZE) { + throw new IllegalArgumentException("Data of wrong length: " + bytes.length); + } + if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) { + throw new IllegalArgumentException("Data cannot be null and have state be initialised"); + } + this.stateUpdating = this.stateVisible = state; + this.storageUpdating = this.storageVisible = bytes; + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("State: "); + switch (this.stateVisible) { + case INIT_STATE_NULL: + stringBuilder.append("null"); + break; + case INIT_STATE_UNINIT: + stringBuilder.append("uninitialised"); + break; + case INIT_STATE_INIT: + stringBuilder.append("initialised"); + break; + case INIT_STATE_HIDDEN: + stringBuilder.append("hidden"); + break; + default: + stringBuilder.append("unknown"); + break; + } + stringBuilder.append("\nData:\n"); + + final byte[] data = this.storageVisible; + if (data != null) { + for (int i = 0; i < 4096; ++i) { + // Copied from NibbleArray#toString + final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF); + + stringBuilder.append(Integer.toHexString(level)); + if ((i & 15) == 15) { + stringBuilder.append("\n"); + } + + if ((i & 255) == 255) { + stringBuilder.append("\n"); + } + } + } else { + stringBuilder.append("null"); + } + + return stringBuilder.toString(); + } + + public SaveState getSaveState() { + synchronized (this) { + final int state = this.stateVisible; + final byte[] data = this.storageVisible; + if (state == INIT_STATE_NULL) { + return null; + } + if (state == INIT_STATE_UNINIT) { + return new SaveState(null, state); + } + final boolean zero = isAllZero(data); + if (zero) { + return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null; + } else { + return new SaveState(data.clone(), state); + } + } + } + + protected static boolean isAllZero(final byte[] data) { + for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) { + byte whole = data[i << 4]; + + for (int k = 1; k < (1 << 4); ++k) { + whole |= data[(i << 4) | k]; + } + + if (whole != 0) { + return false; + } + } + + return true; + } + + // operation type: updating on src, updating on other + public void extrudeLower(final SWMRNibbleArray other) { + if (other.stateUpdating == INIT_STATE_NULL) { + throw new IllegalArgumentException(); + } + + if (other.storageUpdating == null) { + this.setUninitialised(); + return; + } + + final byte[] src = other.storageUpdating; + final byte[] into; + + if (!this.updatingDirty) { + if (this.storageUpdating != null) { + into = this.storageUpdating = allocateBytes(); + } else { + this.storageUpdating = into = allocateBytes(); + this.stateUpdating = INIT_STATE_INIT; + } + this.updatingDirty = true; + } else { + into = this.storageUpdating; + } + + final int start = 0; + final int end = (15 | (15 << 4)) >>> 1; + + /* x | (z << 4) | (y << 8) */ + for (int y = 0; y <= 15; ++y) { + System.arraycopy(src, start, into, y << (8 - 1), end - start + 1); + } + } + + // operation type: updating + public void setFull() { + if (this.stateUpdating != INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + } + Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1); + this.updatingDirty = true; + } + + // operation type: updating + public void setZero() { + if (this.stateUpdating != INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + } + Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0); + this.updatingDirty = true; + } + + // operation type: updating + public void setNonNull() { + if (this.stateUpdating == INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + return; + } + if (this.stateUpdating != INIT_STATE_NULL) { + return; + } + this.stateUpdating = INIT_STATE_UNINIT; + } + + // operation type: updating + public void setNull() { + this.stateUpdating = INIT_STATE_NULL; + if (this.updatingDirty && this.storageUpdating != null) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = null; + this.updatingDirty = false; + } + + // operation type: updating + public void setUninitialised() { + this.stateUpdating = INIT_STATE_UNINIT; + if (this.storageUpdating != null && this.updatingDirty) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = null; + this.updatingDirty = false; + } + + // operation type: updating + public void setHidden() { + if (this.stateUpdating == INIT_STATE_HIDDEN) { + return; + } + if (this.stateUpdating != INIT_STATE_INIT) { + this.setNull(); + } else { + this.stateUpdating = INIT_STATE_HIDDEN; + } + } + + // operation type: updating + public boolean isDirty() { + return this.stateUpdating != this.stateVisible || this.updatingDirty; + } + + // operation type: updating + public boolean isNullNibbleUpdating() { + return this.stateUpdating == INIT_STATE_NULL; + } + + // operation type: visible + public boolean isNullNibbleVisible() { + return this.stateVisible == INIT_STATE_NULL; + } + + // opeartion type: updating + public boolean isUninitialisedUpdating() { + return this.stateUpdating == INIT_STATE_UNINIT; + } + + // operation type: visible + public boolean isUninitialisedVisible() { + return this.stateVisible == INIT_STATE_UNINIT; + } + + // operation type: updating + public boolean isInitialisedUpdating() { + return this.stateUpdating == INIT_STATE_INIT; + } + + // operation type: visible + public boolean isInitialisedVisible() { + return this.stateVisible == INIT_STATE_INIT; + } + + // operation type: updating + public boolean isHiddenUpdating() { + return this.stateUpdating == INIT_STATE_HIDDEN; + } + + // operation type: updating + public boolean isHiddenVisible() { + return this.stateVisible == INIT_STATE_HIDDEN; + } + + // operation type: updating + protected void swapUpdatingAndMarkDirty() { + if (this.updatingDirty) { + return; + } + + if (this.storageUpdating == null) { + this.storageUpdating = allocateBytes(); + Arrays.fill(this.storageUpdating, (byte)0); + } else { + System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE); + } + + if (this.stateUpdating != INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + } + this.updatingDirty = true; + } + + // operation type: updating + public boolean updateVisible() { + if (!this.isDirty()) { + return false; + } + + synchronized (this) { + if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) { + this.storageVisible = null; + } else { + if (this.storageVisible == null) { + this.storageVisible = this.storageUpdating.clone(); + } else { + if (this.storageUpdating != this.storageVisible) { + System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE); + } + } + + if (this.storageUpdating != this.storageVisible) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = this.storageVisible; + } + this.updatingDirty = false; + this.stateVisible = this.stateUpdating; + } + + return true; + } + + // operation type: visible + public DataLayer toVanillaNibble() { + synchronized (this) { + switch (this.stateVisible) { + case INIT_STATE_HIDDEN: + case INIT_STATE_NULL: + return null; + case INIT_STATE_UNINIT: + return new DataLayer(); + case INIT_STATE_INIT: + return new DataLayer(this.storageVisible.clone()); + default: + throw new IllegalStateException(); + } + } + } + + /* x | (z << 4) | (y << 8) */ + + // operation type: updating + public int getUpdating(final int x, final int y, final int z) { + return this.getUpdating((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); + } + + // operation type: updating + public int getUpdating(final int index) { + // indices range from 0 -> 4096 + final byte[] bytes = this.storageUpdating; + if (bytes == null) { + return 0; + } + final byte value = bytes[index >>> 1]; + + // if we are an even index, we want lower 4 bits + // if we are an odd index, we want upper 4 bits + return ((value >>> ((index & 1) << 2)) & 0xF); + } + + // operation type: visible + public int getVisible(final int x, final int y, final int z) { + return this.getVisible((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); + } + + // operation type: visible + public int getVisible(final int index) { + // indices range from 0 -> 4096 + final byte[] visibleBytes = this.storageVisible; + if (visibleBytes == null) { + return 0; + } + final byte value = visibleBytes[index >>> 1]; + + // if we are an even index, we want lower 4 bits + // if we are an odd index, we want upper 4 bits + return ((value >>> ((index & 1) << 2)) & 0xF); + } + + // operation type: updating + public void set(final int x, final int y, final int z, final int value) { + this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value); + } + + // operation type: updating + public void set(final int index, final int value) { + if (!this.updatingDirty) { + this.swapUpdatingAndMarkDirty(); + } + final int shift = (index & 1) << 2; + final int i = index >>> 1; + + this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift)); + } + + public static final class SaveState { + + public final byte[] data; + public final int state; + + public SaveState(final byte[] data, final int state) { + this.data = data; + this.state = state; + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java new file mode 100644 index 0000000..26d6262 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java @@ -0,0 +1,711 @@ +package ca.spottedleaf.moonrise.patches.starlight.light; + +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState; +import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortIterator; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.Arrays; +import java.util.Set; + +public final class SkyStarLightEngine extends StarLightEngine { + + /* + Specification for managing the initialisation and de-initialisation of skylight nibble arrays: + + Skylight nibble initialisation requires that non-empty chunk sections have 1 radius nibbles non-null. + + This presents some problems, as vanilla is only guaranteed to have 0 radius neighbours loaded when editing blocks. + However starlight fixes this so that it has 1 radius loaded. Still, we don't actually have guarantees + that we have the necessary chunks loaded to de-initialise neighbour sections (but we do have enough to de-initialise + our own) - we need a radius of 2 to de-initialise neighbour nibbles. + How do we solve this? + + Each chunk will store the last known "emptiness" of sections for each of their 1 radius neighbour chunk sections. + If the chunk does not have full data, then its nibbles are NOT de-initialised. This is because obviously the + chunk did not go through the light stage yet - or its neighbours are not lit. In either case, once the last + known "emptiness" of neighbouring sections is filled with data, the chunk will run a full check of the data + to see if any of its nibbles need to be de-initialised. + + The emptiness map allows us to de-initialise neighbour nibbles if the neighbour has it filled with data, + and if it doesn't have data then we know it will correctly de-initialise once it fills up. + + Unlike vanilla, we store whether nibbles are uninitialised on disk - so we don't need any dumb hacking + around those. + */ + + protected final int[] heightMapBlockChange = new int[16 * 16]; + { + Arrays.fill(this.heightMapBlockChange, Integer.MIN_VALUE); // clear heightmap + } + + protected final boolean[] nullPropagationCheckCache; + + public SkyStarLightEngine(final Level world) { + super(true, world); + this.nullPropagationCheckCache = new boolean[WorldUtil.getTotalLightSections(world)]; + } + + @Override + protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) { + if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) { + return; + } + SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble == null) { + if (!initRemovedNibbles) { + throw new IllegalStateException(); + } else { + this.setNibbleInCache(chunkX, chunkY, chunkZ, nibble = new SWMRNibbleArray(null, true)); + } + } + this.initNibble(nibble, chunkX, chunkY, chunkZ, extrude); + } + + @Override + protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble != null) { + nibble.setNull(); + } + } + + protected final void initNibble(final SWMRNibbleArray currNibble, final int chunkX, final int chunkY, final int chunkZ, final boolean extrude) { + if (!currNibble.isNullNibbleUpdating()) { + // already initialised + return; + } + + final boolean[] emptinessMap = this.getEmptinessMap(chunkX, chunkZ); + + // are we above this chunk's lowest empty section? + int lowestY = this.minLightSection - 1; + for (int currY = this.maxSection; currY >= this.minSection; --currY) { + if (emptinessMap == null) { + // cannot delay nibble init for lit chunks, as we need to init to propagate into them. + final LevelChunkSection current = this.getChunkSection(chunkX, currY, chunkZ); + if (current == null || current.hasOnlyAir()) { + continue; + } + } else { + if (emptinessMap[currY - this.minSection]) { + continue; + } + } + + // should always be full lit here + lowestY = currY; + break; + } + + if (chunkY > lowestY) { + // we need to set this one to full + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + nibble.setNonNull(); + nibble.setFull(); + return; + } + + if (extrude) { + // this nibble is going to depend solely on the skylight data above it + // find first non-null data above (there does exist one, as we just found it above) + for (int currY = chunkY + 1; currY <= this.maxLightSection; ++currY) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, currY, chunkZ); + if (nibble != null && !nibble.isNullNibbleUpdating()) { + currNibble.setNonNull(); + currNibble.extrudeLower(nibble); + break; + } + } + } else { + currNibble.setNonNull(); + } + } + + protected final void rewriteNibbleCacheForSkylight(final ChunkAccess chunk) { + for (int index = 0, max = this.nibbleCache.length; index < max; ++index) { + final SWMRNibbleArray nibble = this.nibbleCache[index]; + if (nibble != null && nibble.isNullNibbleUpdating()) { + // stop propagation in these areas + this.nibbleCache[index] = null; + nibble.updateVisible(); + } + } + } + + // rets whether neighbours were init'd + + protected final boolean checkNullSection(final int chunkX, final int chunkY, final int chunkZ, + final boolean extrudeInitialised) { + // null chunk sections may have nibble neighbours in the horizontal 1 radius that are + // non-null. Propagation to these neighbours is necessary. + // What makes this easy is we know none of these neighbours are non-empty (otherwise + // this nibble would be initialised). So, we don't have to initialise + // the neighbours in the full 1 radius, because there's no worry that any "paths" + // to the neighbours on this horizontal plane are blocked. + if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.nullPropagationCheckCache[chunkY - this.minLightSection]) { + return false; + } + this.nullPropagationCheckCache[chunkY - this.minLightSection] = true; + + // check horizontal neighbours + boolean needInitNeighbours = false; + neighbour_search: + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, chunkY, dz + chunkZ); + if (nibble != null && !nibble.isNullNibbleUpdating()) { + needInitNeighbours = true; + break neighbour_search; + } + } + } + + if (needInitNeighbours) { + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + this.initNibble(dx + chunkX, chunkY, dz + chunkZ, (dx | dz) == 0 ? extrudeInitialised : true, true); + } + } + } + + return needInitNeighbours; + } + + protected final int getLightLevelExtruded(final int worldX, final int worldY, final int worldZ) { + final int chunkX = worldX >> 4; + int chunkY = worldY >> 4; + final int chunkZ = worldZ >> 4; + + SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble != null) { + return nibble.getUpdating(worldX, worldY, worldZ); + } + + for (;;) { + if (++chunkY > this.maxLightSection) { + return 15; + } + + nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + + if (nibble != null) { + return nibble.getUpdating(worldX, 0, worldZ); + } + } + } + + @Override + protected boolean[] getEmptinessMap(final ChunkAccess chunk) { + return ((StarlightChunk)chunk).getSkyEmptinessMap(); + } + + @Override + protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { + ((StarlightChunk)chunk).setSkyEmptinessMap(to); + } + + @Override + protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { + return ((StarlightChunk)chunk).getSkyNibbles(); + } + + @Override + protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { + ((StarlightChunk)chunk).setSkyNibbles(to); + } + + @Override + protected boolean canUseChunk(final ChunkAccess chunk) { + // can only use chunks for sky stuff if their sections have been init'd + return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); + } + + @Override + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, + final int toSection) { + Arrays.fill(this.nullPropagationCheckCache, false); + this.rewriteNibbleCacheForSkylight(chunk); + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + for (int y = toSection; y >= fromSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, true); + } + + super.checkChunkEdges(lightAccess, chunk, fromSection, toSection); + } + + @Override + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) { + Arrays.fill(this.nullPropagationCheckCache, false); + this.rewriteNibbleCacheForSkylight(chunk); + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) { + final int y = (int)iterator.nextShort(); + this.checkNullSection(chunkX, y, chunkZ, true); + } + + super.checkChunkEdges(lightAccess, chunk, sections); + } + + @Override + protected void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) { + // blocks can change opacity + // blocks can change direction of propagation + + // same logic applies from BlockStarLightEngine#checkBlock + + final int encodeOffset = this.coordinateOffset; + + final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); + + if (currentLevel == 15) { + // must re-propagate clobbered source + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (currentLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the block is conditionally transparent + ); + } else { + this.setLightLevel(worldX, worldY, worldZ, 0); + } + + this.appendToDecreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (currentLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + ); + } + + protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); + protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); + + @Override + protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect) { + if (expect == 15) { + return expect; + } + + final int sectionOffset = this.chunkSectionIndexOffset; + final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); + int opacity = ((StarlightAbstractBlockState)centerState).getOpacityIfCached(); + + final BlockState conditionallyOpaqueState; + if (opacity < 0) { + this.recalcCenterPos.set(worldX, worldY, worldZ); + opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos)); + if (((StarlightAbstractBlockState)centerState).isConditionallyFullOpaque()) { + conditionallyOpaqueState = centerState; + } else { + conditionallyOpaqueState = null; + } + } else { + conditionallyOpaqueState = null; + opacity = Math.max(1, opacity); + } + + int level = 0; + + for (final AxisDirection direction : AXIS_DIRECTIONS) { + final int offX = worldX + direction.x; + final int offY = worldY + direction.y; + final int offZ = worldZ + direction.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + + final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8)); + + if ((neighbourLevel - 1) <= level) { + // don't need to test transparency, we know it wont affect the result. + continue; + } + + final BlockState neighbourState = this.getBlockState(offX, offY, offZ); + + if (((StarlightAbstractBlockState)neighbourState).isConditionallyFullOpaque()) { + // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that + // we don't read the blockstate because most of the time this is false, so using the faster + // known transparency lookup results in a net win + this.recalcNeighbourPos.set(offX, offY, offZ); + final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); + final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); + if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { + // not allowed to propagate + continue; + } + } + + final int calculated = neighbourLevel - opacity; + level = Math.max(calculated, level); + if (level > expect) { + return level; + } + } + + return level; + } + + @Override + protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) { + this.rewriteNibbleCacheForSkylight(atChunk); + Arrays.fill(this.nullPropagationCheckCache, false); + + final BlockGetter world = lightAccess.getLevel(); + final int chunkX = atChunk.getPos().x; + final int chunkZ = atChunk.getPos().z; + final int heightMapOffset = chunkX * -16 + (chunkZ * (-16 * 16)); + + // setup heightmap for changes + for (final BlockPos pos : positions) { + final int index = pos.getX() + (pos.getZ() << 4) + heightMapOffset; + final int curr = this.heightMapBlockChange[index]; + if (pos.getY() > curr) { + this.heightMapBlockChange[index] = pos.getY(); + } + } + + // note: light sets are delayed while processing skylight source changes due to how + // nibbles are initialised, as we want to avoid clobbering nibble values so what when + // below nibbles are initialised they aren't reading from partially modified nibbles + + // now we can recalculate the sources for the changed columns + for (int index = 0; index < (16 * 16); ++index) { + final int maxY = this.heightMapBlockChange[index]; + if (maxY == Integer.MIN_VALUE) { + // not changed + continue; + } + this.heightMapBlockChange[index] = Integer.MIN_VALUE; // restore default for next caller + + final int columnX = (index & 15) | (chunkX << 4); + final int columnZ = (index >>> 4) | (chunkZ << 4); + + // try and propagate from the above y + // delay light set until after processing all sources to setup + final int maxPropagationY = this.tryPropagateSkylight(world, columnX, maxY, columnZ, true, true); + + // maxPropagationY is now the highest block that could not be propagated to + + // remove all sources below that are 15 + final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; + final int encodeOffset = this.coordinateOffset; + + if (this.getLightLevelExtruded(columnX, maxPropagationY, columnZ) == 15) { + // ensure section is checked + this.checkNullSection(columnX >> 4, maxPropagationY >> 4, columnZ >> 4, true); + + for (int currY = maxPropagationY; currY >= (this.minLightSection << 4); --currY) { + if ((currY & 15) == 15) { + // ensure section is checked + this.checkNullSection(columnX >> 4, (currY >> 4), columnZ >> 4, true); + } + + // ensure section below is always checked + final SWMRNibbleArray nibble = this.getNibbleFromCache(columnX >> 4, currY >> 4, columnZ >> 4); + if (nibble == null) { + // advance currY to the the top of the section below + currY = (currY) & (~15); + // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually + // end up there + continue; + } + + if (nibble.getUpdating(columnX, currY, columnZ) != 15) { + break; + } + + // delay light set until after processing all sources to setup + this.appendToDecreaseQueue( + ((columnX + (columnZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) + | (propagateDirection << (6 + 6 + 16 + 4)) + // do not set transparent blocks for the same reason we don't in the checkBlock method + ); + } + } + } + + // delayed light sets are processed here, and must be processed before checkBlock as checkBlock reads + // immediate light value + this.processDelayedIncreases(); + this.processDelayedDecreases(); + + for (final BlockPos pos : positions) { + this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); + } + + this.performLightDecrease(lightAccess); + } + + protected final int[] heightMapGen = new int[32 * 32]; + + @Override + protected void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { + this.rewriteNibbleCacheForSkylight(chunk); + Arrays.fill(this.nullPropagationCheckCache, false); + + final BlockGetter world = lightAccess.getLevel(); + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + final LevelChunkSection[] sections = chunk.getSections(); + + int highestNonEmptySection = this.maxSection; + while (highestNonEmptySection == (this.minSection - 1) || + sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].hasOnlyAir()) { + this.checkNullSection(chunkX, highestNonEmptySection, chunkZ, false); + // try propagate FULL to neighbours + + // check neighbours to see if we need to propagate into them + for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { + final int neighbourX = chunkX + direction.x; + final int neighbourZ = chunkZ + direction.z; + final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(neighbourX, highestNonEmptySection, neighbourZ); + if (neighbourNibble == null) { + // unloaded neighbour + // most of the time we fall here + continue; + } + + // it looks like we need to propagate into the neighbour + + final int incX; + final int incZ; + final int startX; + final int startZ; + + if (direction.x != 0) { + // x direction + incX = 0; + incZ = 1; + + if (direction.x < 0) { + // negative + startX = chunkX << 4; + } else { + startX = chunkX << 4 | 15; + } + startZ = chunkZ << 4; + } else { + // z direction + incX = 1; + incZ = 0; + + if (direction.z < 0) { + // negative + startZ = chunkZ << 4; + } else { + startZ = chunkZ << 4 | 15; + } + startX = chunkX << 4; + } + + final int encodeOffset = this.coordinateOffset; + final long propagateDirection = 1L << direction.ordinal(); // we only want to check in this direction + + for (int currY = highestNonEmptySection << 4, maxY = currY | 15; currY <= maxY; ++currY) { + for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { + this.appendToIncreaseQueue( + ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) // we know we're at full lit here + | (propagateDirection << (6 + 6 + 16 + 4)) + // no transparent flag, we know for a fact there are no blocks here that could be directionally transparent (as the section is EMPTY) + ); + } + } + } + + if (highestNonEmptySection-- == (this.minSection - 1)) { + break; + } + } + + if (highestNonEmptySection >= this.minSection) { + // fill out our other sources + final int minX = chunkPos.x << 4; + final int maxX = chunkPos.x << 4 | 15; + final int minZ = chunkPos.z << 4; + final int maxZ = chunkPos.z << 4 | 15; + final int startY = highestNonEmptySection << 4 | 15; + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + this.tryPropagateSkylight(world, currX, startY + 1, currZ, false, false); + } + } + } // else: apparently the chunk is empty + + if (needsEdgeChecks) { + // not required to propagate here, but this will reduce the hit of the edge checks + this.performLightIncrease(lightAccess); + + for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, false); + } + // no need to rewrite the nibble cache again + super.checkChunkEdges(lightAccess, chunk, this.minLightSection, highestNonEmptySection); + } else { + for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, false); + } + this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, highestNonEmptySection); + + this.performLightIncrease(lightAccess); + } + } + + protected final void processDelayedIncreases() { + // copied from performLightIncrease + final long[] queue = this.increaseQueue; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + + for (int i = 0, len = this.increaseQueueInitialLength; i < len; ++i) { + final long queueValue = queue[i]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); + + this.setLightLevel(posX, posY, posZ, propagatedLightLevel); + } + } + + protected final void processDelayedDecreases() { + // copied from performLightDecrease + final long[] queue = this.decreaseQueue; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + + for (int i = 0, len = this.decreaseQueueInitialLength; i < len; ++i) { + final long queueValue = queue[i]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + + this.setLightLevel(posX, posY, posZ, 0); + } + } + + // delaying the light set is useful for block changes since they need to worry about initialising nibblearrays + // while also queueing light at the same time (initialising nibblearrays might depend on nibbles above, so + // clobbering the light values will result in broken propagation) + protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ, + final boolean extrudeInitialised, final boolean delayLightSet) { + final BlockPos.MutableBlockPos mutablePos = this.mutablePos3; + final int encodeOffset = this.coordinateOffset; + final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards. + + if (this.getLightLevelExtruded(worldX, startY + 1, worldZ) != 15) { + return startY; + } + + // ensure this section is always checked + this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised); + + BlockState above = this.getBlockState(worldX, startY + 1, worldZ); + + for (;startY >= (this.minLightSection << 4); --startY) { + if ((startY & 15) == 15) { + // ensure this section is always checked + this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised); + } + final BlockState current = this.getBlockState(worldX, startY, worldZ); + + final VoxelShape fromShape; + if (((StarlightAbstractBlockState)above).isConditionallyFullOpaque()) { + this.mutablePos2.set(worldX, startY + 1, worldZ); + fromShape = above.getFaceOcclusionShape(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms); + if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { + // above wont let us propagate + break; + } + } else { + fromShape = Shapes.empty(); + } + + final int opacityIfCached = ((StarlightAbstractBlockState)current).getOpacityIfCached(); + // does light propagate from the top down? + if (opacityIfCached != -1) { + if (opacityIfCached != 0) { + // we cannot propagate 15 through this + break; + } + // most of the time it falls here. + // add to propagate + // light set delayed until we determine if this nibble section is null + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) // we know we're at full lit here + | (propagateDirection << (6 + 6 + 16 + 4)) + ); + } else { + mutablePos.set(worldX, startY, worldZ); + long flags = 0L; + if (((StarlightAbstractBlockState)current).isConditionallyFullOpaque()) { + final VoxelShape cullingFace = current.getFaceOcclusionShape(world, mutablePos, AxisDirection.POSITIVE_Y.nms); + + if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { + // can't propagate here, we're done on this column. + break; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = current.getLightBlock(world, mutablePos); + if (opacity > 0) { + // let the queued value (if any) handle it from here. + break; + } + + // light set delayed until we determine if this nibble section is null + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) // we know we're at full lit here + | (propagateDirection << (6 + 6 + 16 + 4)) + | flags + ); + } + + above = current; + + if (this.getNibbleFromCache(worldX >> 4, startY >> 4, worldZ >> 4) == null) { + // we skip empty sections here, as this is just an easy way of making sure the above block + // can propagate through air. + + // nothing can propagate in null sections, remove the queue entry for it + --this.increaseQueueInitialLength; + + // advance currY to the the top of the section below + startY = (startY) & (~15); + // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually + // end up there + + // make sure this is marked as AIR + above = AIR_BLOCK_STATE; + } else if (!delayLightSet) { + this.setLightLevel(worldX, startY, worldZ, 15); + } + } + + return startY; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java new file mode 100644 index 0000000..81753fa --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java @@ -0,0 +1,1573 @@ +package ca.spottedleaf.moonrise.patches.starlight.light; + +import ca.spottedleaf.concurrentutil.util.IntegerUtil; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortIterator; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public abstract class StarLightEngine { + + protected static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState(); + + protected static final AxisDirection[] DIRECTIONS = AxisDirection.values(); + protected static final AxisDirection[] AXIS_DIRECTIONS = DIRECTIONS; + protected static final AxisDirection[] ONLY_HORIZONTAL_DIRECTIONS = new AxisDirection[] { + AxisDirection.POSITIVE_X, AxisDirection.NEGATIVE_X, + AxisDirection.POSITIVE_Z, AxisDirection.NEGATIVE_Z + }; + + protected static enum AxisDirection { + + // Declaration order is important and relied upon. Do not change without modifying propagation code. + POSITIVE_X(1, 0, 0), NEGATIVE_X(-1, 0, 0), + POSITIVE_Z(0, 0, 1), NEGATIVE_Z(0, 0, -1), + POSITIVE_Y(0, 1, 0), NEGATIVE_Y(0, -1, 0); + + static { + POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X; + POSITIVE_Z.opposite = NEGATIVE_Z; NEGATIVE_Z.opposite = POSITIVE_Z; + POSITIVE_Y.opposite = NEGATIVE_Y; NEGATIVE_Y.opposite = POSITIVE_Y; + } + + protected AxisDirection opposite; + + public final int x; + public final int y; + public final int z; + public final Direction nms; + public final long everythingButThisDirection; + public final long everythingButTheOppositeDirection; + + AxisDirection(final int x, final int y, final int z) { + this.x = x; + this.y = y; + this.z = z; + this.nms = Direction.fromDelta(x, y, z); + this.everythingButThisDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << this.ordinal())); + // positive is always even, negative is always odd. Flip the 1 bit to get the negative direction. + this.everythingButTheOppositeDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << (this.ordinal() ^ 1))); + } + + public AxisDirection getOpposite() { + return this.opposite; + } + } + + // I'd like to thank https://www.seedofandromeda.com/blogs/29-fast-flood-fill-lighting-in-a-blocky-voxel-game-pt-1 + // for explaining how light propagates via breadth-first search + + // While the above is a good start to understanding the general idea of what the general principles are, it's not + // exactly how the vanilla light engine should behave for minecraft. + + // similar to the above, except the chunk section indices vary from [-1, 1], or [0, 2] + // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] + // index = x + (z * 5) + (y * 25) + // null index indicates the chunk section doesn't exist (empty or out of bounds) + protected final LevelChunkSection[] sectionCache; + + // the exact same as above, except for storing fast access to SWMRNibbleArray + // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] + // index = x + (z * 5) + (y * 25) + protected final SWMRNibbleArray[] nibbleCache; + + // the exact same as above, except for storing fast access to nibbles to call change callbacks for + // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] + // index = x + (z * 5) + (y * 25) + protected final boolean[] notifyUpdateCache; + + // always initialsed during start of lighting. + // index = x + (z * 5) + protected final ChunkAccess[] chunkCache = new ChunkAccess[5 * 5]; + + // index = x + (z * 5) + protected final boolean[][] emptinessMapCache = new boolean[5 * 5][]; + + protected final BlockPos.MutableBlockPos mutablePos1 = new BlockPos.MutableBlockPos(); + protected final BlockPos.MutableBlockPos mutablePos2 = new BlockPos.MutableBlockPos(); + protected final BlockPos.MutableBlockPos mutablePos3 = new BlockPos.MutableBlockPos(); + + protected int encodeOffsetX; + protected int encodeOffsetY; + protected int encodeOffsetZ; + + protected int coordinateOffset; + + protected int chunkOffsetX; + protected int chunkOffsetY; + protected int chunkOffsetZ; + + protected int chunkIndexOffset; + protected int chunkSectionIndexOffset; + + protected final boolean skylightPropagator; + protected final int emittedLightMask; + protected final boolean isClientSide; + + protected final Level world; + protected final int minLightSection; + protected final int maxLightSection; + protected final int minSection; + protected final int maxSection; + + protected StarLightEngine(final boolean skylightPropagator, final Level world) { + this.skylightPropagator = skylightPropagator; + this.emittedLightMask = skylightPropagator ? 0 : 0xF; + this.isClientSide = world.isClientSide; + this.world = world; + this.minLightSection = WorldUtil.getMinLightSection(world); + this.maxLightSection = WorldUtil.getMaxLightSection(world); + this.minSection = WorldUtil.getMinSection(world); + this.maxSection = WorldUtil.getMaxSection(world); + + this.sectionCache = new LevelChunkSection[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer + this.nibbleCache = new SWMRNibbleArray[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer + this.notifyUpdateCache = new boolean[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer + } + + protected final void setupEncodeOffset(final int centerX, final int centerY, final int centerZ) { + // 31 = center + encodeOffset + this.encodeOffsetX = 31 - centerX; + this.encodeOffsetY = (-(this.minLightSection - 1) << 4); // we want 0 to be the smallest encoded value + this.encodeOffsetZ = 31 - centerZ; + + // coordinateIndex = x | (z << 6) | (y << 12) + this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << 6) + (this.encodeOffsetY << 12); + + // 2 = (centerX >> 4) + chunkOffset + this.chunkOffsetX = 2 - (centerX >> 4); + this.chunkOffsetY = -(this.minLightSection - 1); // lowest should be 0 + this.chunkOffsetZ = 2 - (centerZ >> 4); + + // chunk index = x + (5 * z) + this.chunkIndexOffset = this.chunkOffsetX + (5 * this.chunkOffsetZ); + + // chunk section index = x + (5 * z) + ((5*5) * y) + this.chunkSectionIndexOffset = this.chunkIndexOffset + ((5 * 5) * this.chunkOffsetY); + } + + protected final void setupCaches(final LightChunkGetter chunkProvider, final int centerX, final int centerY, final int centerZ, + final boolean relaxed, final boolean tryToLoadChunksFor2Radius) { + final int centerChunkX = centerX >> 4; + final int centerChunkY = centerY >> 4; + final int centerChunkZ = centerZ >> 4; + + this.setupEncodeOffset(centerChunkX * 16 + 7, centerChunkY * 16 + 7, centerChunkZ * 16 + 7); + + final int radius = tryToLoadChunksFor2Radius ? 2 : 1; + + for (int dz = -radius; dz <= radius; ++dz) { + for (int dx = -radius; dx <= radius; ++dx) { + final int cx = centerChunkX + dx; + final int cz = centerChunkZ + dz; + final boolean isTwoRadius = Math.max(IntegerUtil.branchlessAbs(dx), IntegerUtil.branchlessAbs(dz)) == 2; + final ChunkAccess chunk = (ChunkAccess)chunkProvider.getChunkForLighting(cx, cz); + + if (chunk == null) { + if (relaxed | isTwoRadius) { + continue; + } + throw new IllegalArgumentException("Trying to propagate light update before 1 radius neighbours ready"); + } + + if (!this.canUseChunk(chunk)) { + continue; + } + + this.setChunkInCache(cx, cz, chunk); + this.setEmptinessMapCache(cx, cz, this.getEmptinessMap(chunk)); + if (!isTwoRadius) { + this.setBlocksForChunkInCache(cx, cz, chunk.getSections()); + this.setNibblesForChunkInCache(cx, cz, this.getNibblesOnChunk(chunk)); + } + } + } + } + + protected final ChunkAccess getChunkInCache(final int chunkX, final int chunkZ) { + return this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset]; + } + + protected final void setChunkInCache(final int chunkX, final int chunkZ, final ChunkAccess chunk) { + this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk; + } + + protected final LevelChunkSection getChunkSection(final int chunkX, final int chunkY, final int chunkZ) { + return this.sectionCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset]; + } + + protected final void setChunkSectionInCache(final int chunkX, final int chunkY, final int chunkZ, final LevelChunkSection section) { + this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section; + } + + protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final LevelChunkSection[] sections) { + for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { + this.setChunkSectionInCache(chunkX, cy, chunkZ, + sections == null ? null : (cy >= this.minSection && cy <= this.maxSection ? sections[cy - this.minSection] : null)); + } + } + + protected final SWMRNibbleArray getNibbleFromCache(final int chunkX, final int chunkY, final int chunkZ) { + return this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset]; + } + + protected final SWMRNibbleArray[] getNibblesForChunkFromCache(final int chunkX, final int chunkZ) { + final SWMRNibbleArray[] ret = new SWMRNibbleArray[this.maxLightSection - this.minLightSection + 1]; + + for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { + ret[cy - this.minLightSection] = this.nibbleCache[chunkX + 5*chunkZ + (cy * (5 * 5)) + this.chunkSectionIndexOffset]; + } + + return ret; + } + + protected final void setNibbleInCache(final int chunkX, final int chunkY, final int chunkZ, final SWMRNibbleArray nibble) { + this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset] = nibble; + } + + protected final void setNibblesForChunkInCache(final int chunkX, final int chunkZ, final SWMRNibbleArray[] nibbles) { + for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { + this.setNibbleInCache(chunkX, cy, chunkZ, nibbles == null ? null : nibbles[cy - this.minLightSection]); + } + } + + protected final void updateVisible(final LightChunkGetter lightAccess) { + for (int index = 0, max = this.nibbleCache.length; index < max; ++index) { + final SWMRNibbleArray nibble = this.nibbleCache[index]; + if (!this.notifyUpdateCache[index] && (nibble == null || !nibble.isDirty())) { + continue; + } + + final int chunkX = (index % 5) - this.chunkOffsetX; + final int chunkZ = ((index / 5) % 5) - this.chunkOffsetZ; + final int ySections = this.maxSection - this.minSection + 1; + final int chunkY = ((index / (5*5)) % (ySections + 2 + 2)) - this.chunkOffsetY; + if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) { + lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, chunkY, chunkZ)); + } + } + } + + protected final void destroyCaches() { + Arrays.fill(this.sectionCache, null); + Arrays.fill(this.nibbleCache, null); + Arrays.fill(this.chunkCache, null); + Arrays.fill(this.emptinessMapCache, null); + if (this.isClientSide) { + Arrays.fill(this.notifyUpdateCache, false); + } + } + + protected final BlockState getBlockState(final int worldX, final int worldY, final int worldZ) { + final LevelChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset]; + + if (section != null) { + return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15); + } + + return AIR_BLOCK_STATE; + } + + protected final BlockState getBlockState(final int sectionIndex, final int localIndex) { + final LevelChunkSection section = this.sectionCache[sectionIndex]; + + if (section != null) { + return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.states.get(localIndex); + } + + return AIR_BLOCK_STATE; + } + + protected final int getLightLevel(final int worldX, final int worldY, final int worldZ) { + final SWMRNibbleArray nibble = this.nibbleCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset]; + + return nibble == null ? 0 : nibble.getUpdating((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8)); + } + + protected final int getLightLevel(final int sectionIndex, final int localIndex) { + final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; + + return nibble == null ? 0 : nibble.getUpdating(localIndex); + } + + protected final void setLightLevel(final int worldX, final int worldY, final int worldZ, final int level) { + final int sectionIndex = (worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset; + final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; + + if (nibble != null) { + nibble.set((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8), level); + if (this.isClientSide) { + int cx1 = (worldX - 1) >> 4; + int cx2 = (worldX + 1) >> 4; + int cy1 = (worldY - 1) >> 4; + int cy2 = (worldY + 1) >> 4; + int cz1 = (worldZ - 1) >> 4; + int cz2 = (worldZ + 1) >> 4; + for (int x = cx1; x <= cx2; ++x) { + for (int y = cy1; y <= cy2; ++y) { + for (int z = cz1; z <= cz2; ++z) { + this.notifyUpdateCache[x + 5 * z + (5 * 5) * y + this.chunkSectionIndexOffset] = true; + } + } + } + } + } + } + + protected final void postLightUpdate(final int worldX, final int worldY, final int worldZ) { + if (this.isClientSide) { + int cx1 = (worldX - 1) >> 4; + int cx2 = (worldX + 1) >> 4; + int cy1 = (worldY - 1) >> 4; + int cy2 = (worldY + 1) >> 4; + int cz1 = (worldZ - 1) >> 4; + int cz2 = (worldZ + 1) >> 4; + for (int x = cx1; x <= cx2; ++x) { + for (int y = cy1; y <= cy2; ++y) { + for (int z = cz1; z <= cz2; ++z) { + this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true; + } + } + } + } + } + + protected final void setLightLevel(final int sectionIndex, final int localIndex, final int worldX, final int worldY, final int worldZ, final int level) { + final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; + + if (nibble != null) { + nibble.set(localIndex, level); + if (this.isClientSide) { + int cx1 = (worldX - 1) >> 4; + int cx2 = (worldX + 1) >> 4; + int cy1 = (worldY - 1) >> 4; + int cy2 = (worldY + 1) >> 4; + int cz1 = (worldZ - 1) >> 4; + int cz2 = (worldZ + 1) >> 4; + for (int x = cx1; x <= cx2; ++x) { + for (int y = cy1; y <= cy2; ++y) { + for (int z = cz1; z <= cz2; ++z) { + this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true; + } + } + } + } + } + } + + protected final boolean[] getEmptinessMap(final int chunkX, final int chunkZ) { + return this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset]; + } + + protected final void setEmptinessMapCache(final int chunkX, final int chunkZ, final boolean[] emptinessMap) { + this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = emptinessMap; + } + + public static SWMRNibbleArray[] getFilledEmptyLight(final LevelHeightAccessor world) { + return getFilledEmptyLight(WorldUtil.getTotalLightSections(world)); + } + + private static SWMRNibbleArray[] getFilledEmptyLight(final int totalLightSections) { + final SWMRNibbleArray[] ret = new SWMRNibbleArray[totalLightSections]; + + for (int i = 0, len = ret.length; i < len; ++i) { + ret[i] = new SWMRNibbleArray(null, true); + } + + return ret; + } + + protected abstract boolean[] getEmptinessMap(final ChunkAccess chunk); + + protected abstract void setEmptinessMap(final ChunkAccess chunk, final boolean[] to); + + protected abstract SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk); + + protected abstract void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to); + + protected abstract boolean canUseChunk(final ChunkAccess chunk); + + public final void blocksChangedInChunk(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, + final Set positions, final Boolean[] changedSections) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + if (changedSections != null) { + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, changedSections, false); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + } + if (!positions.isEmpty()) { + this.propagateBlockChanges(lightAccess, chunk, positions); + } + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + protected abstract void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions); + + protected abstract void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ); + + // if ret > expect, then the real value is at least ret (early returns if ret > expect, rather than calculating actual) + // if ret == expect, then expect is the correct light value for pos + // if ret < expect, then ret is the real light value + protected abstract int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect); + + protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16]; + protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16]; + + protected void checkChunkEdge(final LightChunkGetter lightAccess, final ChunkAccess chunk, + final int chunkX, final int chunkY, final int chunkZ) { + final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (currNibble == null) { + return; + } + + for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { + final int neighbourOffX = direction.x; + final int neighbourOffZ = direction.z; + + final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX, + chunkY, chunkZ + neighbourOffZ); + + if (neighbourNibble == null) { + continue; + } + + if (!currNibble.isInitialisedUpdating() && !neighbourNibble.isInitialisedUpdating()) { + // both are zero, nothing to check. + continue; + } + + // this chunk + final int incX; + final int incZ; + final int startX; + final int startZ; + + if (neighbourOffX != 0) { + // x direction + incX = 0; + incZ = 1; + + if (direction.x < 0) { + // negative + startX = chunkX << 4; + } else { + startX = chunkX << 4 | 15; + } + startZ = chunkZ << 4; + } else { + // z direction + incX = 1; + incZ = 0; + + if (neighbourOffZ < 0) { + // negative + startZ = chunkZ << 4; + } else { + startZ = chunkZ << 4 | 15; + } + startX = chunkX << 4; + } + + int centerDelayedChecks = 0; + int neighbourDelayedChecks = 0; + for (int currY = chunkY << 4, maxY = currY | 15; currY <= maxY; ++currY) { + for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { + final int neighbourX = currX + neighbourOffX; + final int neighbourZ = currZ + neighbourOffZ; + + final int currentIndex = (currX & 15) | + ((currZ & 15)) << 4 | + ((currY & 15) << 8); + final int currentLevel = currNibble.getUpdating(currentIndex); + + final int neighbourIndex = + (neighbourX & 15) | + ((neighbourZ & 15)) << 4 | + ((currY & 15) << 8); + final int neighbourLevel = neighbourNibble.getUpdating(neighbourIndex); + + // the checks are delayed because the checkBlock method clobbers light values - which then + // affect later calculate light value operations. While they don't affect it in a behaviourly significant + // way, they do have a negative performance impact due to simply queueing more values + + if (this.calculateLightValue(lightAccess, currX, currY, currZ, currentLevel) != currentLevel) { + this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex; + } + + if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel) != neighbourLevel) { + this.chunkCheckDelayedUpdatesNeighbour[neighbourDelayedChecks++] = neighbourIndex; + } + } + } + + final int currentChunkOffX = chunkX << 4; + final int currentChunkOffZ = chunkZ << 4; + final int neighbourChunkOffX = (chunkX + direction.x) << 4; + final int neighbourChunkOffZ = (chunkZ + direction.z) << 4; + final int chunkOffY = chunkY << 4; + for (int i = 0, len = Math.max(centerDelayedChecks, neighbourDelayedChecks); i < len; ++i) { + // try to queue neighbouring data together + // index = x | (z << 4) | (y << 8) + if (i < centerDelayedChecks) { + final int value = this.chunkCheckDelayedUpdatesCenter[i]; + this.checkBlock(lightAccess, currentChunkOffX | (value & 15), + chunkOffY | (value >>> 8), + currentChunkOffZ | ((value >>> 4) & 0xF)); + } + if (i < neighbourDelayedChecks) { + final int value = this.chunkCheckDelayedUpdatesNeighbour[i]; + this.checkBlock(lightAccess, neighbourChunkOffX | (value & 15), + chunkOffY | (value >>> 8), + neighbourChunkOffZ | ((value >>> 4) & 0xF)); + } + } + } + } + + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) { + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) { + this.checkChunkEdge(lightAccess, chunk, chunkX, iterator.nextShort(), chunkZ); + } + + this.performLightDecrease(lightAccess); + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + // verifies that light levels on this chunks edges are consistent with this chunk's neighbours + // edges. if they are not, they are decreased (effectively performing the logic in checkBlock). + // This does not resolve skylight source problems. + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) { + this.checkChunkEdge(lightAccess, chunk, chunkX, currSectionY, chunkZ); + } + + this.performLightDecrease(lightAccess); + } + + // pulls light from neighbours, and adds them into the increase queue. does not actually propagate. + protected final void propagateNeighbourLevels(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) { + final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, currSectionY, chunkZ); + if (currNibble == null) { + continue; + } + for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { + final int neighbourOffX = direction.x; + final int neighbourOffZ = direction.z; + + final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX, + currSectionY, chunkZ + neighbourOffZ); + + if (neighbourNibble == null || !neighbourNibble.isInitialisedUpdating()) { + // can't pull from 0 + continue; + } + + // neighbour chunk + final int incX; + final int incZ; + final int startX; + final int startZ; + + if (neighbourOffX != 0) { + // x direction + incX = 0; + incZ = 1; + + if (direction.x < 0) { + // negative + startX = (chunkX << 4) - 1; + } else { + startX = (chunkX << 4) + 16; + } + startZ = chunkZ << 4; + } else { + // z direction + incX = 1; + incZ = 0; + + if (neighbourOffZ < 0) { + // negative + startZ = (chunkZ << 4) - 1; + } else { + startZ = (chunkZ << 4) + 16; + } + startX = chunkX << 4; + } + + final long propagateDirection = 1L << direction.getOpposite().ordinal(); // we only want to check in this direction towards this chunk + final int encodeOffset = this.coordinateOffset; + + for (int currY = currSectionY << 4, maxY = currY | 15; currY <= maxY; ++currY) { + for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { + final int level = neighbourNibble.getUpdating( + (currX & 15) + | ((currZ & 15) << 4) + | ((currY & 15) << 8) + ); + + if (level <= 1) { + // nothing to propagate + continue; + } + + this.appendToIncreaseQueue( + ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((level & 0xFL) << (6 + 6 + 16)) + | (propagateDirection << (6 + 6 + 16 + 4)) + | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the current block is transparent, must check. + ); + } + } + } + } + } + + public static Boolean[] getEmptySectionsForChunk(final ChunkAccess chunk) { + final LevelChunkSection[] sections = chunk.getSections(); + final Boolean[] ret = new Boolean[sections.length]; + + for (int i = 0; i < sections.length; ++i) { + if (sections[i] == null || sections[i].hasOnlyAir()) { + ret[i] = Boolean.TRUE; + } else { + ret[i] = Boolean.FALSE; + } + } + + return ret; + } + + public final void forceHandleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptinessChanges) { + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + try { + // force current chunk into cache + this.setChunkInCache(chunkX, chunkZ, chunk); + this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections()); + this.setNibblesForChunkInCache(chunkX, chunkZ, this.getNibblesOnChunk(chunk)); + this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk)); + + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + public final void handleEmptySectionChanges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, + final Boolean[] emptinessChanges) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + protected abstract void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles); + + protected abstract void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ); + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + // subclasses are guaranteed that this is always called before a changed block set + // newChunk specifies whether the changes describe a "first load" of a chunk or changes to existing, already loaded chunks + // rets non-null when the emptiness map changed and needs to be updated + protected final boolean[] handleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, + final Boolean[] emptinessChanges, final boolean unlit) { + final Level world = (Level)lightAccess.getLevel(); + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + + boolean[] chunkEmptinessMap = this.getEmptinessMap(chunkX, chunkZ); + boolean[] ret = null; + final boolean needsInit = unlit || chunkEmptinessMap == null; + if (needsInit) { + this.setEmptinessMapCache(chunkX, chunkZ, ret = chunkEmptinessMap = new boolean[WorldUtil.getTotalSections(world)]); + } + + // update emptiness map + for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) { + Boolean valueBoxed = emptinessChanges[sectionIndex]; + if (valueBoxed == null) { + if (!needsInit) { + continue; + } + final LevelChunkSection section = this.getChunkSection(chunkX, sectionIndex + this.minSection, chunkZ); + emptinessChanges[sectionIndex] = valueBoxed = section == null || section.hasOnlyAir() ? Boolean.TRUE : Boolean.FALSE; + } + chunkEmptinessMap[sectionIndex] = valueBoxed.booleanValue(); + } + + // now init neighbour nibbles + for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) { + final Boolean valueBoxed = emptinessChanges[sectionIndex]; + final int sectionY = sectionIndex + this.minSection; + if (valueBoxed == null) { + continue; + } + + final boolean empty = valueBoxed.booleanValue(); + + if (empty) { + continue; + } + + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + // if we're not empty, we also need to initialise nibbles + // note: if we're unlit, we absolutely do not want to extrude, as light data isn't set up + final boolean extrude = (dx | dz) != 0 || !unlit; + for (int dy = 1; dy >= -1; --dy) { + this.initNibble(dx + chunkX, dy + sectionY, dz + chunkZ, extrude, false); + } + } + } + } + + // check for de-init and lazy-init + // lazy init is when chunks are being lit, so at the time they weren't loaded when their neighbours were running + // init checks. + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + // does this neighbour have 1 radius loaded? + boolean neighboursLoaded = true; + neighbour_loaded_search: + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + if (this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ) == null) { + neighboursLoaded = false; + break neighbour_loaded_search; + } + } + } + + for (int sectionY = this.maxLightSection; sectionY >= this.minLightSection; --sectionY) { + // check neighbours to see if we need to de-init this one + boolean allEmpty = true; + neighbour_search: + for (int dy2 = -1; dy2 <= 1; ++dy2) { + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + final int y = sectionY + dy2; + if (y < this.minSection || y > this.maxSection) { + // empty + continue; + } + final boolean[] emptinessMap = this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ); + if (emptinessMap != null) { + if (!emptinessMap[y - this.minSection]) { + allEmpty = false; + break neighbour_search; + } + } else { + final LevelChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ); + if (section != null && !section.hasOnlyAir()) { + allEmpty = false; + break neighbour_search; + } + } + } + } + } + + if (allEmpty & neighboursLoaded) { + // can only de-init when neighbours are loaded + // de-init is fine to delay, as de-init is just an optimisation - it's not required for lighting + // to be correct + + // all were empty, so de-init + this.setNibbleNull(dx + chunkX, sectionY, dz + chunkZ); + } else if (!allEmpty) { + // must init + final boolean extrude = (dx | dz) != 0 || !unlit; + this.initNibble(dx + chunkX, sectionY, dz + chunkZ, extrude, false); + } + } + } + } + + return ret; + } + + public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + this.checkChunkEdges(lightAccess, chunk, sections); + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + // needsEdgeChecks applies when possibly loading vanilla data, which means we need to validate the current + // chunks light values with respect to neighbours + // subclasses should note that the emptiness changes are propagated BEFORE this is called, so this function + // does not need to detect empty chunks itself (and it should do no handling for them either!) + protected abstract void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks); + + public final void light(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptySections) { + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + + try { + final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.maxLightSection - this.minLightSection + 1); + // force current chunk into cache + this.setChunkInCache(chunkX, chunkZ, chunk); + this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections()); + this.setNibblesForChunkInCache(chunkX, chunkZ, nibbles); + this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk)); + + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptySections, true); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + this.lightChunk(lightAccess, chunk, true); + this.setNibbles(chunk, nibbles); + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + public final void relightChunks(final LightChunkGetter lightAccess, final Set chunks, + final Consumer chunkLightCallback, final IntConsumer onComplete) { + // it's recommended for maximum performance that the set is ordered according to a BFS from the center of + // the region of chunks to relight + // it's required that tickets are added for each chunk to keep them loaded + final Long2ObjectOpenHashMap nibblesByChunk = new Long2ObjectOpenHashMap<>(); + final Long2ObjectOpenHashMap emptinessMapByChunk = new Long2ObjectOpenHashMap<>(); + + final int[] neighbourLightOrder = new int[] { + // d = 0 + 0, 0, + // d = 1 + -1, 0, + 0, -1, + 1, 0, + 0, 1, + // d = 2 + -1, 1, + 1, 1, + -1, -1, + 1, -1, + }; + + int lightCalls = 0; + + for (final ChunkPos chunkPos : chunks) { + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + final ChunkAccess chunk = (ChunkAccess)lightAccess.getChunkForLighting(chunkX, chunkZ); + if (chunk == null || !this.canUseChunk(chunk)) { + throw new IllegalStateException(); + } + + for (int i = 0, len = neighbourLightOrder.length; i < len; i += 2) { + final int dx = neighbourLightOrder[i]; + final int dz = neighbourLightOrder[i + 1]; + final int neighbourX = dx + chunkX; + final int neighbourZ = dz + chunkZ; + + final ChunkAccess neighbour = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX, neighbourZ); + if (neighbour == null || !this.canUseChunk(neighbour)) { + continue; + } + + if (nibblesByChunk.get(CoordinateUtils.getChunkKey(neighbourX, neighbourZ)) != null) { + // lit already called for neighbour, no need to light it now + continue; + } + + // light neighbour chunk + this.setupEncodeOffset(neighbourX * 16 + 7, 128, neighbourZ * 16 + 7); + try { + // insert all neighbouring chunks for this neighbour that we have data for + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + final int neighbourX2 = neighbourX + dx2; + final int neighbourZ2 = neighbourZ + dz2; + final long key = CoordinateUtils.getChunkKey(neighbourX2, neighbourZ2); + final ChunkAccess neighbour2 = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX2, neighbourZ2); + if (neighbour2 == null || !this.canUseChunk(neighbour2)) { + continue; + } + + final SWMRNibbleArray[] nibbles = nibblesByChunk.get(key); + if (nibbles == null) { + // we haven't lit this chunk + continue; + } + + this.setChunkInCache(neighbourX2, neighbourZ2, neighbour2); + this.setBlocksForChunkInCache(neighbourX2, neighbourZ2, neighbour2.getSections()); + this.setNibblesForChunkInCache(neighbourX2, neighbourZ2, nibbles); + this.setEmptinessMapCache(neighbourX2, neighbourZ2, emptinessMapByChunk.get(key)); + } + } + + final long key = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); + + // now insert the neighbour chunk and light it + final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.world); + nibblesByChunk.put(key, nibbles); + + this.setChunkInCache(neighbourX, neighbourZ, neighbour); + this.setBlocksForChunkInCache(neighbourX, neighbourZ, neighbour.getSections()); + this.setNibblesForChunkInCache(neighbourX, neighbourZ, nibbles); + + final boolean[] neighbourEmptiness = this.handleEmptySectionChanges(lightAccess, neighbour, getEmptySectionsForChunk(neighbour), true); + emptinessMapByChunk.put(key, neighbourEmptiness); + if (chunks.contains(new ChunkPos(neighbourX, neighbourZ))) { + this.setEmptinessMap(neighbour, neighbourEmptiness); + } + + this.lightChunk(lightAccess, neighbour, false); + } finally { + this.destroyCaches(); + } + } + + // done lighting all neighbours, so the chunk is now fully lit + + // make sure nibbles are fully updated before calling back + final SWMRNibbleArray[] nibbles = nibblesByChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + for (final SWMRNibbleArray nibble : nibbles) { + nibble.updateVisible(); + } + + this.setNibbles(chunk, nibbles); + + for (int y = this.minLightSection; y <= this.maxLightSection; ++y) { + lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkX)); + } + + // now do callback + if (chunkLightCallback != null) { + chunkLightCallback.accept(chunkPos); + } + ++lightCalls; + } + + if (onComplete != null) { + onComplete.accept(lightCalls); + } + } + + // contains: + // lower (6 + 6 + 16) = 28 bits: encoded coordinate position (x | (z << 6) | (y << (6 + 6)))) + // next 4 bits: propagated light level (0, 15] + // next 6 bits: propagation direction bitset + // next 24 bits: unused + // last 3 bits: state flags + // state flags: + // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading light + // updates for block sources + protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 2; + // whether the propagation needs to check if its current level is equal to the expected level + // used only in increase propagation + protected static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 1; + // whether the propagation needs to consider if its block is conditionally transparent + protected static final long FLAG_HAS_SIDED_TRANSPARENT_BLOCKS = Long.MIN_VALUE; + + protected long[] increaseQueue = new long[16 * 16 * 16]; + protected int increaseQueueInitialLength; + protected long[] decreaseQueue = new long[16 * 16 * 16]; + protected int decreaseQueueInitialLength; + + protected final long[] resizeIncreaseQueue() { + return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2); + } + + protected final long[] resizeDecreaseQueue() { + return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2); + } + + protected final void appendToIncreaseQueue(final long value) { + final int idx = this.increaseQueueInitialLength++; + long[] queue = this.increaseQueue; + if (idx >= queue.length) { + queue = this.resizeIncreaseQueue(); + queue[idx] = value; + } else { + queue[idx] = value; + } + } + + protected final void appendToDecreaseQueue(final long value) { + final int idx = this.decreaseQueueInitialLength++; + long[] queue = this.decreaseQueue; + if (idx >= queue.length) { + queue = this.resizeDecreaseQueue(); + queue[idx] = value; + } else { + queue[idx] = value; + } + } + + protected static final AxisDirection[][] OLD_CHECK_DIRECTIONS = new AxisDirection[1 << 6][]; + protected static final int ALL_DIRECTIONS_BITSET = (1 << 6) - 1; + static { + for (int i = 0; i < OLD_CHECK_DIRECTIONS.length; ++i) { + final List directions = new ArrayList<>(); + for (int bitset = i, len = Integer.bitCount(i), index = 0; index < len; ++index, bitset ^= IntegerUtil.getTrailingBit(bitset)) { + directions.add(AXIS_DIRECTIONS[IntegerUtil.trailingZeros(bitset)]); + } + OLD_CHECK_DIRECTIONS[i] = directions.toArray(new AxisDirection[0]); + } + } + + protected final void performLightIncrease(final LightChunkGetter lightAccess) { + final BlockGetter world = lightAccess.getLevel(); + long[] queue = this.increaseQueue; + int queueReadIndex = 0; + int queueLength = this.increaseQueueInitialLength; + this.increaseQueueInitialLength = 0; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + final int encodeOffset = this.coordinateOffset; + final int sectionOffset = this.chunkSectionIndexOffset; + + while (queueReadIndex < queueLength) { + final long queueValue = queue[queueReadIndex++]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xFL); + final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63L)]; + + if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) { + if (this.getLightLevel(posX, posY, posZ) != propagatedLightLevel) { + // not at the level we expect, so something changed. + continue; + } + } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) { + // these are used to restore block sources after a propagation decrease + this.setLightLevel(posX, posY, posZ, propagatedLightLevel); + } + + if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) { + // we don't need to worry about our state here. + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int currentLevel; + if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) { + continue; // already at the level we want or unloaded + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = ((StarlightAbstractBlockState)blockState).getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached); + if (targetLevel > currentLevel) { + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); + continue; + } + } + continue; + } else { + this.mutablePos1.set(offX, offY, offZ); + long flags = 0; + if (((StarlightAbstractBlockState)blockState).isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(world, this.mutablePos1); + final int targetLevel = propagatedLightLevel - Math.max(1, opacity); + if (targetLevel <= currentLevel) { + continue; + } + + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) + | (flags); + } + continue; + } + } + } else { + // we actually need to worry about our state here + final BlockState fromBlock = this.getBlockState(posX, posY, posZ); + this.mutablePos2.set(posX, posY, posZ); + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); + + if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { + continue; + } + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int currentLevel; + + if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) { + continue; // already at the level we want + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = ((StarlightAbstractBlockState)blockState).getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached); + if (targetLevel > currentLevel) { + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); + continue; + } + } + continue; + } else { + this.mutablePos1.set(offX, offY, offZ); + long flags = 0; + if (((StarlightAbstractBlockState)blockState).isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(world, this.mutablePos1); + final int targetLevel = propagatedLightLevel - Math.max(1, opacity); + if (targetLevel <= currentLevel) { + continue; + } + + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) + | (flags); + } + continue; + } + } + } + } + } + + protected final void performLightDecrease(final LightChunkGetter lightAccess) { + final BlockGetter world = lightAccess.getLevel(); + long[] queue = this.decreaseQueue; + long[] increaseQueue = this.increaseQueue; + int queueReadIndex = 0; + int queueLength = this.decreaseQueueInitialLength; + this.decreaseQueueInitialLength = 0; + int increaseQueueLength = this.increaseQueueInitialLength; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + final int encodeOffset = this.coordinateOffset; + final int sectionOffset = this.chunkSectionIndexOffset; + final int emittedMask = this.emittedLightMask; + + while (queueReadIndex < queueLength) { + final long queueValue = queue[queueReadIndex++]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); + final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63)]; + + if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) { + // we don't need to worry about our state here. + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int lightLevel; + + if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { + // already at lowest (or unloaded), nothing we can do + continue; + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = ((StarlightAbstractBlockState)blockState).getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | FLAG_RECHECK_LEVEL; + continue; + } + final int emittedLight = blockState.getLightEmission() & emittedMask; + if (emittedLight != 0) { + // re-propagate source + // note: do not set recheck level, or else the propagation will fail + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (((StarlightAbstractBlockState)blockState).isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL); + } + + currentNibble.set(localIndex, 0); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); + continue; + } + continue; + } else { + this.mutablePos1.set(offX, offY, offZ); + long flags = 0; + if (((StarlightAbstractBlockState)blockState).isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(world, this.mutablePos1); + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (FLAG_RECHECK_LEVEL | flags); + continue; + } + final int emittedLight = blockState.getLightEmission() & emittedMask; + if (emittedLight != 0) { + // re-propagate source + // note: do not set recheck level, or else the propagation will fail + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (flags | FLAG_WRITE_LEVEL); + } + + currentNibble.set(localIndex, 0); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) + | flags; + } + continue; + } + } + } else { + // we actually need to worry about our state here + final BlockState fromBlock = this.getBlockState(posX, posY, posZ); + this.mutablePos2.set(posX, posY, posZ); + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); + + if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { + continue; + } + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int lightLevel; + + if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { + // already at lowest (or unloaded), nothing we can do + continue; + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = ((StarlightAbstractBlockState)blockState).getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | FLAG_RECHECK_LEVEL; + continue; + } + final int emittedLight = blockState.getLightEmission() & emittedMask; + if (emittedLight != 0) { + // re-propagate source + // note: do not set recheck level, or else the propagation will fail + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (((StarlightAbstractBlockState)blockState).isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL); + } + + currentNibble.set(localIndex, 0); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); + continue; + } + continue; + } else { + this.mutablePos1.set(offX, offY, offZ); + long flags = 0; + if (((StarlightAbstractBlockState)blockState).isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(world, this.mutablePos1); + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (FLAG_RECHECK_LEVEL | flags); + continue; + } + final int emittedLight = blockState.getLightEmission() & emittedMask; + if (emittedLight != 0) { + // re-propagate source + // note: do not set recheck level, or else the propagation will fail + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (flags | FLAG_WRITE_LEVEL); + } + + currentNibble.set(localIndex, 0); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) + | flags; + } + continue; + } + } + } + } + + // propagate sources we clobbered + this.increaseQueueInitialLength = increaseQueueLength; + this.performLightIncrease(lightAccess); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java new file mode 100644 index 0000000..4e27f77 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java @@ -0,0 +1,669 @@ +package ca.spottedleaf.moonrise.patches.starlight.light; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; +import ca.spottedleaf.moonrise.patches.starlight.world.StarlightWorld; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.DataLayer; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.level.lighting.LayerLightEventListener; +import net.minecraft.world.level.lighting.LevelLightEngine; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public final class StarLightInterface { + + public static final TicketType CHUNK_WORK_TICKET = TicketType.create("starlight_chunk_work_ticket", (p1, p2) -> Long.compare(p1.toLong(), p2.toLong())); + + /** + * Can be {@code null}, indicating the light is all empty. + */ + protected final Level world; + protected final LightChunkGetter lightAccess; + + protected final ArrayDeque cachedSkyPropagators; + protected final ArrayDeque cachedBlockPropagators; + + protected final LightQueue lightQueue = new LightQueue(this); + + protected final LayerLightEventListener skyReader; + protected final LayerLightEventListener blockReader; + protected final boolean isClientSide; + + protected final int minSection; + protected final int maxSection; + protected final int minLightSection; + protected final int maxLightSection; + + public final LevelLightEngine lightEngine; + + private final boolean hasBlockLight; + private final boolean hasSkyLight; + + public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) { + this.lightAccess = lightAccess; + this.world = lightAccess == null ? null : (Level)lightAccess.getLevel(); + this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null; + this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null; + this.isClientSide = !(this.world instanceof ServerLevel); + if (this.world == null) { + this.minSection = -4; + this.maxSection = 19; + this.minLightSection = -5; + this.maxLightSection = 20; + } else { + this.minSection = WorldUtil.getMinSection(this.world); + this.maxSection = WorldUtil.getMaxSection(this.world); + this.minLightSection = WorldUtil.getMinLightSection(this.world); + this.maxLightSection = WorldUtil.getMaxLightSection(this.world); + } + this.lightEngine = lightEngine; + this.hasBlockLight = hasBlockLight; + this.hasSkyLight = hasSkyLight; + this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { + @Override + public void checkBlock(final BlockPos blockPos) { + StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); + } + + @Override + public void propagateLightSources(final ChunkPos chunkPos) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasLightWork() { + // not really correct... + return StarLightInterface.this.hasUpdates(); + } + + @Override + public int runLightUpdates() { + throw new UnsupportedOperationException(); + } + + @Override + public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) { + throw new UnsupportedOperationException(); + } + + @Override + public DataLayer getDataLayerData(final SectionPos pos) { + final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); + if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { + return null; + } + + final int sectionY = pos.getY(); + + if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) { + return null; + } + + if (((StarlightChunk)chunk).getSkyEmptinessMap() == null) { + return null; + } + + return ((StarlightChunk)chunk).getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble(); + } + + @Override + public int getLightValue(final BlockPos blockPos) { + return StarLightInterface.this.getSkyLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4)); + } + + @Override + public void updateSectionStatus(final SectionPos pos, final boolean notReady) { + StarLightInterface.this.sectionChange(pos, notReady); + } + }; + this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { + @Override + public void checkBlock(final BlockPos blockPos) { + StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); + } + + @Override + public void propagateLightSources(final ChunkPos chunkPos) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasLightWork() { + // not really correct... + return StarLightInterface.this.hasUpdates(); + } + + @Override + public int runLightUpdates() { + throw new UnsupportedOperationException(); + } + + @Override + public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) { + throw new UnsupportedOperationException(); + } + + @Override + public DataLayer getDataLayerData(final SectionPos pos) { + final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); + + if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) { + return null; + } + + return ((StarlightChunk)chunk).getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble(); + } + + @Override + public int getLightValue(final BlockPos blockPos) { + return StarLightInterface.this.getBlockLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4)); + } + + @Override + public void updateSectionStatus(final SectionPos pos, final boolean notReady) { + StarLightInterface.this.sectionChange(pos, notReady); + } + }; + } + + public boolean hasSkyLight() { + return this.hasSkyLight; + } + + public boolean hasBlockLight() { + return this.hasBlockLight; + } + + public int getSkyLightValue(final BlockPos blockPos, final ChunkAccess chunk) { + if (!this.hasSkyLight) { + return 0; + } + final int x = blockPos.getX(); + int y = blockPos.getY(); + final int z = blockPos.getZ(); + + final int minSection = this.minSection; + final int maxSection = this.maxSection; + final int minLightSection = this.minLightSection; + final int maxLightSection = this.maxLightSection; + + if (chunk == null || (!this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { + return 15; + } + + int sectionY = y >> 4; + + if (sectionY > maxLightSection) { + return 15; + } + + if (sectionY < minLightSection) { + sectionY = minLightSection; + y = sectionY << 4; + } + + final SWMRNibbleArray[] nibbles = ((StarlightChunk)chunk).getSkyNibbles(); + final SWMRNibbleArray immediate = nibbles[sectionY - minLightSection]; + + if (!immediate.isNullNibbleVisible()) { + return immediate.getVisible(x, y, z); + } + + final boolean[] emptinessMap = ((StarlightChunk)chunk).getSkyEmptinessMap(); + + if (emptinessMap == null) { + return 15; + } + + // are we above this chunk's lowest empty section? + int lowestY = minLightSection - 1; + for (int currY = maxSection; currY >= minSection; --currY) { + if (emptinessMap[currY - minSection]) { + continue; + } + + // should always be full lit here + lowestY = currY; + break; + } + + if (sectionY > lowestY) { + return 15; + } + + // this nibble is going to depend solely on the skylight data above it + // find first non-null data above (there does exist one, as we just found it above) + for (int currY = sectionY + 1; currY <= maxLightSection; ++currY) { + final SWMRNibbleArray nibble = nibbles[currY - minLightSection]; + if (!nibble.isNullNibbleVisible()) { + return nibble.getVisible(x, 0, z); + } + } + + // should never reach here + return 15; + } + + public int getBlockLightValue(final BlockPos blockPos, final ChunkAccess chunk) { + if (!this.hasBlockLight) { + return 0; + } + final int y = blockPos.getY(); + final int cy = y >> 4; + + final int minLightSection = this.minLightSection; + final int maxLightSection = this.maxLightSection; + + if (cy < minLightSection || cy > maxLightSection) { + return 0; + } + + if (chunk == null) { + return 0; + } + + final SWMRNibbleArray nibble = ((StarlightChunk)chunk).getBlockNibbles()[cy - minLightSection]; + return nibble.getVisible(blockPos.getX(), y, blockPos.getZ()); + } + + public int getRawBrightness(final BlockPos pos, final int ambientDarkness) { + final ChunkAccess chunk = this.getAnyChunkNow(pos.getX() >> 4, pos.getZ() >> 4); + + final int sky = this.getSkyLightValue(pos, chunk) - ambientDarkness; + // Don't fetch the block light level if the skylight level is 15, since the value will never be higher. + if (sky == 15) { + return 15; + } + final int block = this.getBlockLightValue(pos, chunk); + return Math.max(sky, block); + } + + public LayerLightEventListener getSkyReader() { + return this.skyReader; + } + + public LayerLightEventListener getBlockReader() { + return this.blockReader; + } + + public boolean isClientSide() { + return this.isClientSide; + } + + public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) { + if (this.world == null) { + // empty world + return null; + } + return ((StarlightWorld)this.world).getAnyChunkImmediately(chunkX, chunkZ); + } + + public boolean hasUpdates() { + return !this.lightQueue.isEmpty(); + } + + public Level getWorld() { + return this.world; + } + + public LightChunkGetter getLightAccess() { + return this.lightAccess; + } + + protected final SkyStarLightEngine getSkyLightEngine() { + if (this.cachedSkyPropagators == null) { + return null; + } + final SkyStarLightEngine ret; + synchronized (this.cachedSkyPropagators) { + ret = this.cachedSkyPropagators.pollFirst(); + } + + if (ret == null) { + return new SkyStarLightEngine(this.world); + } + return ret; + } + + protected final void releaseSkyLightEngine(final SkyStarLightEngine engine) { + if (this.cachedSkyPropagators == null) { + return; + } + synchronized (this.cachedSkyPropagators) { + this.cachedSkyPropagators.addFirst(engine); + } + } + + protected final BlockStarLightEngine getBlockLightEngine() { + if (this.cachedBlockPropagators == null) { + return null; + } + final BlockStarLightEngine ret; + synchronized (this.cachedBlockPropagators) { + ret = this.cachedBlockPropagators.pollFirst(); + } + + if (ret == null) { + return new BlockStarLightEngine(this.world); + } + return ret; + } + + protected final void releaseBlockLightEngine(final BlockStarLightEngine engine) { + if (this.cachedBlockPropagators == null) { + return; + } + synchronized (this.cachedBlockPropagators) { + this.cachedBlockPropagators.addFirst(engine); + } + } + + public LightQueue.ChunkTasks blockChange(final BlockPos pos) { + if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world + return null; + } + + return this.lightQueue.queueBlockChange(pos); + } + + public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) { + if (this.world == null) { // empty world + return null; + } + + return this.lightQueue.queueSectionChange(pos, newEmptyValue); + } + + public void forceLoadInChunk(final ChunkAccess chunk, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); + } + if (blockEngine != null) { + blockEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void loadInChunk(final int chunkX, final int chunkZ, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); + } + if (blockEngine != null) { + blockEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void lightChunk(final ChunkAccess chunk, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.light(this.lightAccess, chunk, emptySections); + } + if (blockEngine != null) { + blockEngine.light(this.lightAccess, chunk, emptySections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void relightChunks(final Set chunks, final Consumer chunkLightCallback, + final IntConsumer onComplete) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.relightChunks(this.lightAccess, chunks, blockEngine == null ? chunkLightCallback : null, + blockEngine == null ? onComplete : null); + } + if (blockEngine != null) { + blockEngine.relightChunks(this.lightAccess, chunks, chunkLightCallback, onComplete); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void checkChunkEdges(final int chunkX, final int chunkZ) { + this.checkSkyEdges(chunkX, chunkZ); + this.checkBlockEdges(chunkX, chunkZ); + } + + public void checkSkyEdges(final int chunkX, final int chunkZ) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + } + } + + public void checkBlockEdges(final int chunkX, final int chunkZ) { + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + try { + if (blockEngine != null) { + blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); + } + } finally { + this.releaseBlockLightEngine(blockEngine); + } + } + + public void checkSkyEdges(final int chunkX, final int chunkZ, final ShortCollection sections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + } + } + + public void checkBlockEdges(final int chunkX, final int chunkZ, final ShortCollection sections) { + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + try { + if (blockEngine != null) { + blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections); + } + } finally { + this.releaseBlockLightEngine(blockEngine); + } + } + + public void scheduleChunkLight(final ChunkPos pos, final Runnable run) { + this.lightQueue.queueChunkLighting(pos, run); + } + + public void removeChunkTasks(final ChunkPos pos) { + this.lightQueue.removeChunk(pos); + } + + public void propagateChanges() { + if (this.lightQueue.isEmpty()) { + return; + } + + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + LightQueue.ChunkTasks task; + while ((task = this.lightQueue.removeFirstTask()) != null) { + if (task.lightTasks != null) { + for (final Runnable run : task.lightTasks) { + run.run(); + } + } + + final long coordinate = task.chunkCoordinate; + final int chunkX = CoordinateUtils.getChunkX(coordinate); + final int chunkZ = CoordinateUtils.getChunkZ(coordinate); + + final Set positions = task.changedPositions; + final Boolean[] sectionChanges = task.changedSectionSet; + + if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { + skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); + } + if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { + blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); + } + + if (skyEngine != null && task.queuedEdgeChecksSky != null) { + skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksSky); + } + if (blockEngine != null && task.queuedEdgeChecksBlock != null) { + blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksBlock); + } + + task.onComplete.complete(null); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public static final class LightQueue { + + protected final Long2ObjectLinkedOpenHashMap chunkTasks = new Long2ObjectLinkedOpenHashMap<>(); + protected final StarLightInterface manager; + + public LightQueue(final StarLightInterface manager) { + this.manager = manager; + } + + public synchronized boolean isEmpty() { + return this.chunkTasks.isEmpty(); + } + + public synchronized LightQueue.ChunkTasks queueBlockChange(final BlockPos pos) { + final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + tasks.changedPositions.add(pos.immutable()); + return tasks; + } + + public synchronized LightQueue.ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { + final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + + if (tasks.changedSectionSet == null) { + tasks.changedSectionSet = new Boolean[this.manager.maxSection - this.manager.minSection + 1]; + } + tasks.changedSectionSet[pos.getY() - this.manager.minSection] = Boolean.valueOf(newEmptyValue); + + return tasks; + } + + public synchronized LightQueue.ChunkTasks queueChunkLighting(final ChunkPos pos, final Runnable lightTask) { + final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + if (tasks.lightTasks == null) { + tasks.lightTasks = new ArrayList<>(); + } + tasks.lightTasks.add(lightTask); + + return tasks; + } + + public synchronized LightQueue.ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { + final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + + ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksSky; + if (queuedEdges == null) { + queuedEdges = tasks.queuedEdgeChecksSky = new ShortOpenHashSet(); + } + queuedEdges.addAll(sections); + + return tasks; + } + + public synchronized LightQueue.ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { + final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + + ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksBlock; + if (queuedEdges == null) { + queuedEdges = tasks.queuedEdgeChecksBlock = new ShortOpenHashSet(); + } + queuedEdges.addAll(sections); + + return tasks; + } + + public void removeChunk(final ChunkPos pos) { + final ChunkTasks tasks; + synchronized (this) { + tasks = this.chunkTasks.remove(CoordinateUtils.getChunkKey(pos)); + } + if (tasks != null) { + tasks.onComplete.complete(null); + } + } + + public synchronized ChunkTasks removeFirstTask() { + if (this.chunkTasks.isEmpty()) { + return null; + } + return this.chunkTasks.removeFirst(); + } + + public static final class ChunkTasks { + + public final Set changedPositions = new ObjectOpenHashSet<>(); + public Boolean[] changedSectionSet; + public ShortOpenHashSet queuedEdgeChecksSky; + public ShortOpenHashSet queuedEdgeChecksBlock; + public List lightTasks; + + public boolean isTicketAdded = false; + public final CompletableFuture onComplete = new CompletableFuture<>(); + + public final long chunkCoordinate; + + public ChunkTasks(final long chunkCoordinate) { + this.chunkCoordinate = chunkCoordinate; + } + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java new file mode 100644 index 0000000..c053b1e --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java @@ -0,0 +1,20 @@ +package ca.spottedleaf.moonrise.patches.starlight.light; + +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.chunk.DataLayer; +import net.minecraft.world.level.chunk.LevelChunk; + +public interface StarLightLightingProvider { + + public StarLightInterface getLightEngine(); + + public void clientUpdateLight(final LightLayer lightType, final SectionPos pos, + final DataLayer nibble, final boolean trustEdges); + + public void clientRemoveLightData(final ChunkPos chunkPos); + + public void clientChunkLoad(final ChunkPos pos, final LevelChunk chunk); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java new file mode 100644 index 0000000..bdcdcbd --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java @@ -0,0 +1,194 @@ +package ca.spottedleaf.moonrise.patches.starlight.util; + +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; +import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine; +import com.mojang.logging.LogUtils; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import org.slf4j.Logger; + +public final class SaveUtil { + + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final int STARLIGHT_LIGHT_VERSION = 9; + + public static int getLightVersion() { + return STARLIGHT_LIGHT_VERSION; + } + + private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state"; + private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; + private static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; + + public static void saveLightHook(final Level world, final ChunkAccess chunk, final CompoundTag nbt) { + try { + saveLightHookReal(world, chunk, nbt); + } catch (final Throwable ex) { + // failing to inject is not fatal so we catch anything here. if it fails, it will have correctly set lit to false + // for Vanilla to relight on load and it will not set our lit tag so we will relight on load + if (ex instanceof ThreadDeath) { + throw (ThreadDeath)ex; + } + LOGGER.warn("Failed to inject light data into save data for chunk " + chunk.getPos() + ", chunk light will be recalculated on its next load", ex); + } + } + + private static void saveLightHookReal(final Level world, final ChunkAccess chunk, final CompoundTag tag) { + if (tag == null) { + return; + } + + final int minSection = WorldUtil.getMinLightSection(world); + final int maxSection = WorldUtil.getMaxLightSection(world); + + SWMRNibbleArray[] blockNibbles = ((StarlightChunk)chunk).getBlockNibbles(); + SWMRNibbleArray[] skyNibbles = ((StarlightChunk)chunk).getSkyNibbles(); + + boolean lit = chunk.isLightCorrect() || !(world instanceof ServerLevel); + // diff start - store our tag for whether light data is init'd + if (lit) { + tag.putBoolean("isLightOn", false); + } + // diff end - store our tag for whether light data is init'd + ChunkStatus status = ChunkStatus.byName(tag.getString("Status")); + + CompoundTag[] sections = new CompoundTag[maxSection - minSection + 1]; + + ListTag sectionsStored = tag.getList("sections", 10); + + for (int i = 0; i < sectionsStored.size(); ++i) { + CompoundTag sectionStored = sectionsStored.getCompound(i); + int k = sectionStored.getByte("Y"); + + // strip light data + sectionStored.remove("BlockLight"); + sectionStored.remove("SkyLight"); + + if (!sectionStored.isEmpty()) { + sections[k - minSection] = sectionStored; + } + } + + if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { + for (int i = minSection; i <= maxSection; ++i) { + SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState(); + SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState(); + if (blockNibble != null || skyNibble != null) { + CompoundTag section = sections[i - minSection]; + if (section == null) { + section = new CompoundTag(); + section.putByte("Y", (byte)i); + sections[i - minSection] = section; + } + + // we store under the same key so mod programs editing nbt + // can still read the data, hopefully. + // however, for compatibility we store chunks as unlit so vanilla + // is forced to re-light them if it encounters our data. It's too much of a burden + // to try and maintain compatibility with a broken and inferior skylight management system. + + if (blockNibble != null) { + if (blockNibble.data != null) { + section.putByteArray("BlockLight", blockNibble.data); + } + section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state); + } + + if (skyNibble != null) { + if (skyNibble.data != null) { + section.putByteArray("SkyLight", skyNibble.data); + } + section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state); + } + } + } + } + + // rewrite section list + sectionsStored.clear(); + for (CompoundTag section : sections) { + if (section != null) { + sectionsStored.add(section); + } + } + tag.put("sections", sectionsStored); + if (lit) { + tag.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // only mark as fully lit after we have successfully injected our data + } + } + + public static void loadLightHook(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) { + try { + loadLightHookReal(world, pos, tag, into); + } catch (final Throwable ex) { + // failing to inject is not fatal so we catch anything here. if it fails, then we simply relight. Not a problem, we get correct + // lighting in both cases. + if (ex instanceof ThreadDeath) { + throw (ThreadDeath)ex; + } + LOGGER.warn("Failed to load light for chunk " + pos + ", light will be recalculated", ex); + } + } + + private static void loadLightHookReal(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) { + if (into == null) { + return; + } + final int minSection = WorldUtil.getMinLightSection(world); + final int maxSection = WorldUtil.getMaxLightSection(world); + + into.setLightCorrect(false); // mark as unlit in case we fail parsing + + SWMRNibbleArray[] blockNibbles = StarLightEngine.getFilledEmptyLight(world); + SWMRNibbleArray[] skyNibbles = StarLightEngine.getFilledEmptyLight(world); + + + // start copy from the original method + boolean lit = tag.get("isLightOn") != null && tag.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; + boolean canReadSky = world.dimensionType().hasSkyLight(); + ChunkStatus status = ChunkStatus.byName(tag.getString("Status")); + if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { // diff - we add the status check here + ListTag sections = tag.getList("sections", 10); + + for (int i = 0; i < sections.size(); ++i) { + CompoundTag sectionData = sections.getCompound(i); + int y = sectionData.getByte("Y"); + + if (sectionData.contains("BlockLight", 7)) { + // this is where our diff is + blockNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety + } else { + blockNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG)); + } + + if (canReadSky) { + if (sectionData.contains("SkyLight", 7)) { + // we store under the same key so mod programs editing nbt + // can still read the data, hopefully. + // however, for compatibility we store chunks as unlit so vanilla + // is forced to re-light them if it encounters our data. It's too much of a burden + // to try and maintain compatibility with a broken and inferior skylight management system. + skyNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety + } else { + skyNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG)); + } + } + } + } + // end copy from vanilla + + ((StarlightChunk)into).setBlockNibbles(blockNibbles); + ((StarlightChunk)into).setSkyNibbles(skyNibbles); + into.setLightCorrect(lit); // now we set lit here, only after we've correctly parsed data + } + + private SaveUtil() {} +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/world/StarlightWorld.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/world/StarlightWorld.java new file mode 100644 index 0000000..92cf119 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/world/StarlightWorld.java @@ -0,0 +1,14 @@ +package ca.spottedleaf.moonrise.patches.starlight.world; + +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; + +public interface StarlightWorld { + + // rets full chunk without blocking + public LevelChunk getChunkAtImmediately(final int chunkX, final int chunkZ); + + // rets chunk at any stage, if it exists, immediately + public ChunkAccess getAnyChunkImmediately(final int chunkX, final int chunkZ); + +} diff --git a/src/main/resources/assets/moonrise/icon.png b/src/main/resources/assets/moonrise/icon.png new file mode 100644 index 0000000..538626e Binary files /dev/null and b/src/main/resources/assets/moonrise/icon.png differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..a702929 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": 1, + "id": "moonrise", + "version": "${version}", + "name": "Moonrise", + "description": "", + "authors": [ + "Spottedleaf" + ], + "contact": { + "issues": "https://github.com/Spottedleaf/Moonrise/issues", + "sources": "https://github.com/Spottedleaf/Moonrise", + "discord": "https://discord.gg/tuinity", + "homepage": "https://www.curseforge.com/minecraft/mc-mods/moonrise" + }, + "breaks": { + "notenoughcrashes": "*", + "starlight": "*" + }, + "license": "GPL-3.0-only", + "icon": "assets/moonrise/icon.png", + "environment": "*", + "entrypoints": {}, + "mixins": [ + "moonrise.mixins.json" + ], + "accessWidener" : "moonrise.accesswidener", + "depends": { + "fabricloader": ">=0.14.19", + "minecraft": "1.20.*" + } +} diff --git a/src/main/resources/moonrise.accesswidener b/src/main/resources/moonrise.accesswidener new file mode 100644 index 0000000..c4b3e1a --- /dev/null +++ b/src/main/resources/moonrise.accesswidener @@ -0,0 +1,110 @@ +accessWidener v1 named + +# BlockStateBase.ShapeCache +accessible class net/minecraft/world/level/block/state/BlockBehaviour$BlockStateBase$Cache +accessible field net/minecraft/world/level/block/state/BlockBehaviour$BlockStateBase$Cache lightBlock I +accessible field net/minecraft/world/level/block/state/BlockBehaviour$BlockStateBase$Cache collisionShape Lnet/minecraft/world/phys/shapes/VoxelShape; +accessible field net/minecraft/world/level/block/state/BlockBehaviour$BlockStateBase$Cache occlusionShapes [Lnet/minecraft/world/phys/shapes/VoxelShape; + +# LevelChunkSection +accessible field net/minecraft/world/level/chunk/LevelChunkSection states Lnet/minecraft/world/level/chunk/PalettedContainer; + + +# PalettedContainer +accessible method net/minecraft/world/level/chunk/PalettedContainer get (I)Ljava/lang/Object; + + +# ChunkMap +accessible field net/minecraft/server/level/ChunkMap level Lnet/minecraft/server/level/ServerLevel; +accessible field net/minecraft/server/level/ChunkMap mainThreadExecutor Lnet/minecraft/util/thread/BlockableEventLoop; + +accessible method net/minecraft/server/level/ChunkMap getUpdatingChunkIfPresent (J)Lnet/minecraft/server/level/ChunkHolder; +accessible method net/minecraft/server/level/ChunkMap getVisibleChunkIfPresent (J)Lnet/minecraft/server/level/ChunkHolder; +accessible method net/minecraft/server/level/ChunkMap getChunkQueueLevel (J)Ljava/util/function/IntSupplier; +accessible method net/minecraft/server/level/ChunkMap releaseLightTicket (Lnet/minecraft/world/level/ChunkPos;)V + + +# ChunkHolder +accessible field net/minecraft/server/level/ChunkHolder chunkToSave Ljava/util/concurrent/CompletableFuture; + +# LevelLightEngine +mutable field net/minecraft/world/level/lighting/LevelLightEngine blockEngine Lnet/minecraft/world/level/lighting/LightEngine; +mutable field net/minecraft/world/level/lighting/LevelLightEngine skyEngine Lnet/minecraft/world/level/lighting/LightEngine; + +# ThreadedLevelLightEngine +accessible class net/minecraft/server/level/ThreadedLevelLightEngine$TaskType + + +# SectionStorage +accessible field net/minecraft/world/level/chunk/storage/SectionStorage levelHeightAccessor Lnet/minecraft/world/level/LevelHeightAccessor; +accessible method net/minecraft/world/level/chunk/storage/SectionStorage get (J)Ljava/util/Optional; +accessible method net/minecraft/world/level/chunk/storage/SectionStorage getOrLoad (J)Ljava/util/Optional; + + +# PoiSection +accessible field net/minecraft/world/entity/ai/village/poi/PoiSection byType Ljava/util/Map; + + +# PoiRecord +accessible method net/minecraft/world/entity/ai/village/poi/PoiRecord acquireTicket ()Z +accessible method net/minecraft/world/entity/ai/village/poi/PoiRecord releaseTicket ()Z + + +# OffsetDoubleList +accessible field net/minecraft/world/phys/shapes/OffsetDoubleList delegate Lit/unimi/dsi/fastutil/doubles/DoubleList; +accessible field net/minecraft/world/phys/shapes/OffsetDoubleList offset D + + +# VoxelShapeArray +accessible method net/minecraft/world/phys/shapes/ArrayVoxelShape (Lnet/minecraft/world/phys/shapes/DiscreteVoxelShape;Lit/unimi/dsi/fastutil/doubles/DoubleList;Lit/unimi/dsi/fastutil/doubles/DoubleList;Lit/unimi/dsi/fastutil/doubles/DoubleList;)V + + +# VoxelShape +accessible field net/minecraft/world/phys/shapes/VoxelShape shape Lnet/minecraft/world/phys/shapes/DiscreteVoxelShape; +accessible method net/minecraft/world/phys/shapes/VoxelShape getCoords (Lnet/minecraft/core/Direction$Axis;)Lit/unimi/dsi/fastutil/doubles/DoubleList; + + +# AABB +accessible method net/minecraft/world/phys/AABB getDirection (Lnet/minecraft/world/phys/AABB;Lnet/minecraft/world/phys/Vec3;[DLnet/minecraft/core/Direction;DDD)Lnet/minecraft/core/Direction; + + +# PersistentEntitySectionManager +accessible class net/minecraft/world/level/entity/PersistentEntitySectionManager$Callback + + +# TransientEntitySectionManager +accessible class net/minecraft/world/level/entity/TransientEntitySectionManager$Callback + + +# BitSetDiscreteVoxelShape +accessible field net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape storage Ljava/util/BitSet; +accessible field net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape xMin I +accessible field net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape yMin I +accessible field net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape zMin I +accessible field net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape xMax I +accessible field net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape yMax I +accessible field net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape zMax I +accessible method net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape join (Lnet/minecraft/world/phys/shapes/DiscreteVoxelShape;Lnet/minecraft/world/phys/shapes/DiscreteVoxelShape;Lnet/minecraft/world/phys/shapes/IndexMerger;Lnet/minecraft/world/phys/shapes/IndexMerger;Lnet/minecraft/world/phys/shapes/IndexMerger;Lnet/minecraft/world/phys/shapes/BooleanOp;)Lnet/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape; + + +# IndexMerger +accessible class net/minecraft/world/phys/shapes/IndexMerger + + +# CubeVoxelShape +accessible method net/minecraft/world/phys/shapes/CubeVoxelShape (Lnet/minecraft/world/phys/shapes/DiscreteVoxelShape;)V + + +# ServerGamePacketListenerImpl +accessible method net/minecraft/server/network/ServerGamePacketListenerImpl isSingleplayerOwner ()Z + + + +# ClipContext +accessible field net/minecraft/world/level/ClipContext block Lnet/minecraft/world/level/ClipContext$Block; +accessible field net/minecraft/world/level/ClipContext fluid Lnet/minecraft/world/level/ClipContext$Fluid; + + +# Fluid +accessible method net/minecraft/world/level/material/Fluid isEmpty ()Z +accessible method net/minecraft/world/level/material/Fluid createLegacyBlock (Lnet/minecraft/world/level/material/FluidState;)Lnet/minecraft/world/level/block/state/BlockState; \ No newline at end of file diff --git a/src/main/resources/moonrise.mixins.json b/src/main/resources/moonrise.mixins.json new file mode 100644 index 0000000..6d300fb --- /dev/null +++ b/src/main/resources/moonrise.mixins.json @@ -0,0 +1,66 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "ca.spottedleaf.moonrise.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "bitstorage.SimpleBitStorageMixin", + "bitstorage.ZeroBitStorageMixin", + "chunk_getblock.LevelChunkMixin", + "collisions.ArmorStandMixin", + "collisions.ArrayVoxelShapeMixin", + "collisions.BlockMixin", + "collisions.BlockStateBaseMixin", + "collisions.CollisionGetterMixin", + "collisions.CubeVoxelShapeMixin", + "collisions.DirectionMixin", + "collisions.DiscreteVoxelShapeMixin", + "collisions.EntityGetterMixin", + "collisions.EntityMixin", + "collisions.LevelChunkSectionMixin", + "collisions.LevelMixin", + "collisions.LivingEntityMixin", + "collisions.PersistentEntitySectionManagerCallbackMixin", + "collisions.PersistentEntitySectionManagerMixin", + "collisions.ServerEntityMixin", + "collisions.ShapesMixin", + "collisions.SliceShapeMixin", + "collisions.TransientEntitySectionManagerCallbackMixin", + "collisions.TransientEntitySectionManagerMixin", + "collisions.VoxelShapeMixin", + "datawatcher.SynchedEntityDataMixin", + "explosions.ExplosionMixin", + "explosions.ExplosionProfileMixin", + "fluid.FlowingFluidMixin", + "fluid.FluidMixin", + "fluid.FluidStateMixin", + "hopper.HopperBlockEntityMixin", + "keep_alive_client.ServerGamePacketListenerImplMixin", + "poi_lookup.AcquirePoiMixin", + "poi_lookup.PoiManagerMixin", + "poi_lookup.PortalForcerMixin", + "starlight.blockstate.BlockStateBaseMixin", + "starlight.chunk.ChunkAccessMixin", + "starlight.chunk.EmptyLevelChunkMixin", + "starlight.chunk.ImposterProtoChunkMixin", + "starlight.chunk.LevelChunkMixin", + "starlight.chunk.ProtoChunkMixin", + "starlight.lightengine.LevelLightEngineMixin", + "starlight.lightengine.ThreadedLevelLightEngineMixin", + "starlight.world.ChunkSerializerMixin", + "starlight.world.LevelMixin", + "starlight.world.ServerWorldMixin", + "util_thread_counts.UtilMixin", + "util_time_source.UtilMixin" + ], + "client": [ + "collisions.LiquidBlockRendererMixin", + "collisions.ParticleMixin", + "profiler.MinecraftMixin", + "starlight.multiplayer.ClientPacketListenerMixin", + "starlight.world.ClientLevelMixin" + ], + "injectors": { + "defaultRequire": 1 + } +}