Initial Commit

This commit is contained in:
Spottedleaf
2023-07-29 16:00:23 -07:00
commit 7c753abdb6
143 changed files with 29365 additions and 0 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
patreon: Spottedleaf

118
.gitignore vendored Normal file
View File

@@ -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

675
LICENSE.md Normal file
View File

@@ -0,0 +1,675 @@
### GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
### Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom
to share and change all versions of a program--to make sure it remains
free software for all its users. We, the Free Software Foundation, use
the GNU General Public License for most of our software; it applies
also to any other work released this way by its authors. You can apply
it to your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you
have certain responsibilities if you distribute copies of the
software, or if you modify it: responsibilities to respect the freedom
of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the
manufacturer can do so. This is fundamentally incompatible with the
aim of protecting users' freedom to change the software. The
systematic pattern of such abuse occurs in the area of products for
individuals to use, which is precisely where it is most unacceptable.
Therefore, we have designed this version of the GPL to prohibit the
practice for those products. If such problems arise substantially in
other domains, we stand ready to extend this provision to those
domains in future versions of the GPL, as needed to protect the
freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish
to avoid the special danger that patents applied to a free program
could make it effectively proprietary. To prevent this, the GPL
assures that patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
### TERMS AND CONDITIONS
#### 0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds
of works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of
an exact copy. The resulting work is called a "modified version" of
the earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user
through a computer network, with no transfer of a copy, is not
conveying.
An interactive user interface displays "Appropriate Legal Notices" to
the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
#### 1. Source Code.
The "source code" for a work means the preferred form of the work for
making modifications to it. "Object code" means any non-source form of
a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users can
regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same
work.
#### 2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey,
without conditions so long as your license otherwise remains in force.
You may convey covered works to others for the sole purpose of having
them make modifications exclusively for you, or provide you with
facilities for running those works, provided that you comply with the
terms of this License in conveying all material for which you do not
control copyright. Those thus making or running the covered works for
you must do so exclusively on your behalf, under your direction and
control, on terms that prohibit them from making any copies of your
copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the
conditions stated below. Sublicensing is not allowed; section 10 makes
it unnecessary.
#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such
circumvention is effected by exercising rights under this License with
respect to the covered work, and you disclaim any intention to limit
operation or modification of the work as a means of enforcing, against
the work's users, your or third parties' legal rights to forbid
circumvention of technological measures.
#### 4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
#### 5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these
conditions:
- a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
- b) The work must carry prominent notices stating that it is
released under this License and any conditions added under
section 7. This requirement modifies the requirement in section 4
to "keep intact all notices".
- c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
- d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
#### 6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of
sections 4 and 5, provided that you also convey the machine-readable
Corresponding Source under the terms of this License, in one of these
ways:
- a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
- b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the Corresponding
Source from a network server at no charge.
- c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
- d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
- e) Convey the object code using peer-to-peer transmission,
provided you inform other peers where the object code and
Corresponding Source of the work are being offered to the general
public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal,
family, or household purposes, or (2) anything designed or sold for
incorporation into a dwelling. In determining whether a product is a
consumer product, doubtful cases shall be resolved in favor of
coverage. For a particular product received by a particular user,
"normally used" refers to a typical or common use of that class of
product, regardless of the status of the particular user or of the way
in which the particular user actually uses, or expects or is expected
to use, the product. A product is a consumer product regardless of
whether the product has substantial commercial, industrial or
non-consumer uses, unless such uses represent the only significant
mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to
install and execute modified versions of a covered work in that User
Product from a modified version of its Corresponding Source. The
information must suffice to ensure that the continued functioning of
the modified object code is in no case prevented or interfered with
solely because modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or
updates for a work that has been modified or installed by the
recipient, or for the User Product in which it has been modified or
installed. Access to a network may be denied when the modification
itself materially and adversely affects the operation of the network
or violates the rules and protocols for communication across the
network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
#### 7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders
of that material) supplement the terms of this License with terms:
- a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
- b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
- c) Prohibiting misrepresentation of the origin of that material,
or requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
- d) Limiting the use for publicity purposes of names of licensors
or authors of the material; or
- e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
- f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions
of it) with contractual assumptions of liability to the recipient,
for any liability that these contractual assumptions directly
impose on those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions; the
above requirements apply either way.
#### 8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your license
from a particular copyright holder is reinstated (a) provisionally,
unless and until the copyright holder explicitly and finally
terminates your license, and (b) permanently, if the copyright holder
fails to notify you of the violation by some reasonable means prior to
60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
#### 9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run
a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
#### 10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
#### 11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned
or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within the
scope of its coverage, prohibits the exercise of, or is conditioned on
the non-exercise of one or more of the rights that are specifically
granted under this License. You may not convey a covered work if you
are a party to an arrangement with a third party that is in the
business of distributing software, under which you make payment to the
third party based on the extent of your activity of conveying the
work, and under which the third party grants, to any of the parties
who would receive the covered work from you, a discriminatory patent
license (a) in connection with copies of the covered work conveyed by
you (or copies made from those copies), or (b) primarily for and in
connection with specific products or compilations that contain the
covered work, unless you entered into that arrangement, or that patent
license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
#### 12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under
this License and any other pertinent obligations, then as a
consequence you may not convey it at all. For example, if you agree to
terms that obligate you to collect a royalty for further conveying
from those to whom you convey the Program, the only way you could
satisfy both those terms and this License would be to refrain entirely
from conveying the Program.
#### 13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
#### 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions
of the GNU General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in
detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies that a certain numbered version of the GNU General Public
License "or any later version" applies to it, you have the option of
following the terms and conditions either of that numbered version or
of any later version published by the Free Software Foundation. If the
Program does not specify a version number of the GNU General Public
License, you may choose any version ever published by the Free
Software Foundation.
If the Program specifies that a proxy can decide which future versions
of the GNU General Public License can be used, that proxy's public
statement of acceptance of a version permanently authorizes you to
choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
#### 15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.
#### 16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
#### 17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
### How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these
terms.
To do so, attach the following notices to the program. It is safest to
attach them to the start of each source file to most effectively state
the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper
mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands \`show w' and \`show c' should show the
appropriate parts of the General Public License. Of course, your
program's commands might be different; for a GUI interface, you would
use an "about box".
You should also get your employer (if you work as a programmer) or
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. For more information on this, and how to apply and follow
the GNU GPL, see <https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your
program into proprietary programs. If your program is a subroutine
library, you may consider it more useful to permit linking proprietary
applications with the library. If this is what you want to do, use the
GNU Lesser General Public License instead of this License. But first,
please read <https://www.gnu.org/licenses/why-not-lgpl.html>.

97
build.gradle Normal file
View File

@@ -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()
}
}

12
gradle.properties Normal file
View File

@@ -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

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -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

240
gradlew vendored Normal file
View File

@@ -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" "$@"

91
gradlew.bat vendored Normal file
View File

@@ -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

10
settings.gradle Normal file
View File

@@ -0,0 +1,10 @@
pluginManagement {
repositories {
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
mavenCentral()
gradlePluginPortal()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<E> {
// always non-null
protected LinkedNode<E> head;
// always non-null
protected LinkedNode<E> tail;
/* IMPL NOTE: Leave hashCode and equals to their defaults */
public SRSWLinkedQueue() {
final LinkedNode<E> dummy = new LinkedNode<>(null, null);
this.head = this.tail = dummy;
}
/**
* Must be the reader thread.
*
* <p>
* Returns, without removing, the first element of this queue.
* </p>
* @return Returns, without removing, the first element of this queue.
*/
public E peekFirst() {
LinkedNode<E> 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.
*
* <p>
* Returns and removes the first element of this queue.
* </p>
* @return Returns and removes the first element of this queue.
*/
public E poll() {
LinkedNode<E> 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<E> next = head.getNextAcquire();
this.head = next == null ? head : next;
return ret;
}
/**
* Must be the writer thread.
*
* <p>
* Adds the element to the end of the queue.
* </p>
*
* @throws NullPointerException If the provided element is null
*/
public void addLast(final E element) {
Validate.notNull(element, "Provided element cannot be null");
final LinkedNode<E> append = new LinkedNode<>(element, null);
this.tail.setNextRelease(append);
this.tail = append;
}
protected static final class LinkedNode<E> {
protected volatile Object element;
protected volatile LinkedNode<E> 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<E> 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<E> getNextPlain() {
return (LinkedNode<E>)NEXT_HANDLE.get(this);
}
@SuppressWarnings("unchecked")
protected final LinkedNode<E> getNextAcquire() {
return (LinkedNode<E>)NEXT_HANDLE.getAcquire(this);
}
protected final void setNextPlain(final LinkedNode<E> next) {
NEXT_HANDLE.set(this, next);
}
protected final void setNextRelease(final LinkedNode<E> next) {
NEXT_HANDLE.setRelease(this, next);
}
}
}

View File

@@ -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<T> {
private static final Logger LOGGER = LogUtils.getLogger();
private final MultiThreadedQueue<BiConsumer<T, Throwable>> 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<T, Throwable> 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<T, Throwable> waiter;
while ((waiter = this.waiters.pollOrBlockAdds()) != null) {
this.completeWaiter(waiter, result, throwable);
}
}
private void completeWaiter(final BiConsumer<T, Throwable> 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<T, Throwable> 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<T, Throwable> waiter;
private CancellableImpl(final BiConsumer<T, Throwable> waiter) {
this.waiter = waiter;
}
@Override
public boolean cancel() {
return Completable.this.waiters.remove(this.waiter);
}
}
}

View File

@@ -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()}
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
* <p>
* Note: Interruptions to the the current thread have no effect. Interrupt status is also not affected by this cal.
* </p>
*
* @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.
* <p>
* If there is a task with priority {@link PrioritisedExecutor.Priority#BLOCKING} available, then that such task is executed.
* </p>
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
*
* @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}.
* <p>
* WARNING: This function is <i>not</i> suitable for waiting until a deadline!
* Use {@link #executeUntil(long)} or {@link #executeConditionally(BooleanSupplier, long)} instead.
* </p>
*/
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.
* <p>
* This operation is atomic with respect to other shutdown calls
* </p>
* <p>
* After this call has completed, regardless of return value, this queue will be shutdown.
* </p>
*
* @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.
* <p>
* Exceptions thrown from the runnable will be rethrown.
* </p>
*
* @return {@code true} if this task was executed, {@code false} if it was already marked as completed.
*/
public boolean execute();
}
}

View File

@@ -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();
}

View File

@@ -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();
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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.
* <p>
* 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.
* </p>
*/
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.
* <p>
* This function is MT-Safe.
* </p>
* @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)}.
* <p>
* This is not safe to call with {@link #close(boolean, boolean)} if <code>wait = true</code>, in which case
* the waiting thread may block indefinitely.
* </p>
* <p>
* This function is MT-Safe.
* </p>
* @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);
}
}

View File

@@ -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<PrioritisedPoolExecutorImpl> queues = new TreeSet<>(PrioritisedPoolExecutorImpl.comparator());
protected final String name;
protected final long queueMaxHoldTime;
protected final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> nonShutdownQueues = new ReferenceOpenHashSet<>();
protected final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> 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<Thread, Integer> threadModifier) {
this(name, threads, threadModifier, DEFAULT_QUEUE_HOLD_TIME); // 5ms
}
public PrioritisedThreadPool(final String name, final int threads, final BiConsumer<Thread, Integer> 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<PrioritisedPoolExecutorImpl> 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<PrioritisedPoolExecutorImpl> 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<PrioritisedPoolExecutorImpl> 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<PrioritisedPoolExecutorImpl> 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<PrioritisedPoolExecutorImpl> 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<PrioritisedPoolExecutorImpl> 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<PrioritisedPoolExecutorImpl> 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<PrioritisedPoolExecutorImpl> 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<PrioritisedPoolExecutorImpl> 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;
}
}
}

View File

@@ -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<PrioritisedTask>[] 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<PrioritisedTask>[] queues = this.queues;
synchronized (queues) {
final int max = minPriority.priority;
for (int i = 0; i <= max; ++i) {
final ArrayDeque<PrioritisedTask> 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;
}
}
}

View File

@@ -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<Coordinate, Node> 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<Coordinate> 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<Coordinate> 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<Coordinate> 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<Coordinate> 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<Thread> {
private final ReentrantAreaLock lock;
private final List<Coordinate> areaAffected;
private final Thread thread;
//private final Throwable WHO_CREATED_MY_ASS = new Throwable();
private Node(final ReentrantAreaLock lock, final List<Coordinate> 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<Coordinate> {
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) + "]";
}
}
}

View File

@@ -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<Node> 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<Thread> {
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;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<? super SWMRInt2IntHashTable.TableEntry> 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}
* <p>
* 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.
* </p>
*/
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);
}
}
}

View File

@@ -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<V> {
protected int size;
protected TableEntry<V>[] 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<V>[] getTablePlain() {
//noinspection unchecked
return (TableEntry<V>[])TABLE_HANDLE.get(this);
}
protected final TableEntry<V>[] getTableAcquire() {
//noinspection unchecked
return (TableEntry<V>[])TABLE_HANDLE.getAcquire(this);
}
protected final void setTablePlain(final TableEntry<V>[] table) {
TABLE_HANDLE.set(this, table);
}
protected final void setTableRelease(final TableEntry<V>[] 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<V>[] 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<V> 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<V> 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<V> 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<V> getEntryForOpaque(final long key) {
final int hash = SWMRLong2ObjectHashTable.getHash(key);
final TableEntry<V>[] table = this.getTableAcquire();
for (TableEntry<V> curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) {
if (key == curr.key) {
return curr;
}
}
return null;
}
protected final TableEntry<V> getEntryForPlain(final long key) {
final int hash = SWMRLong2ObjectHashTable.getHash(key);
final TableEntry<V>[] table = this.getTablePlain();
for (TableEntry<V> 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<V>[] table = this.getTableAcquire();
for (int i = 0, len = table.length; i < len; ++i) {
for (TableEntry<V> 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<V>[] table = this.getTableAcquire();
for (int i = 0, len = table.length; i < len; ++i) {
for (TableEntry<V> 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<V> clone() {
return new SWMRLong2ObjectHashTable<>(this.getTableAcquire().length, this.loadFactor, this);
}
/**
* {@inheritDoc}
*/
public void forEach(final Consumer<? super SWMRLong2ObjectHashTable.TableEntry<V>> action) {
Validate.notNull(action, "Null action");
final TableEntry<V>[] table = this.getTableAcquire();
for (int i = 0, len = table.length; i < len; ++i) {
for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
action.accept(curr);
}
}
}
@FunctionalInterface
public static interface BiLongObjectConsumer<V> {
public void accept(final long key, final V value);
}
/**
* {@inheritDoc}
*/
public void forEach(final BiLongObjectConsumer<? super V> action) {
Validate.notNull(action, "Null action");
final TableEntry<V>[] table = this.getTableAcquire();
for (int i = 0, len = table.length; i < len; ++i) {
for (TableEntry<V> 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<V>[] table = this.getTableAcquire();
for (int i = 0, len = table.length; i < len; ++i) {
for (TableEntry<V> 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<? super V> action) {
Validate.notNull(action, "Null action");
final TableEntry<V>[] table = this.getTableAcquire();
for (int i = 0, len = table.length; i < len; ++i) {
for (TableEntry<V> 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<V> 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<V> 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<V>[] 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<V>[] newTable = new TableEntry[newCapacity];
final int indexMask = newCapacity - 1;
for (int i = 0, len = table.length; i < len; ++i) {
for (TableEntry<V> 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<V> insert = new TableEntry<>(key, entry.getValuePlain());
final TableEntry<V> 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<V>[] table = this.getTablePlain();
final int hash = SWMRLong2ObjectHashTable.getHash(key);
final int index = hash & (table.length - 1);
final TableEntry<V> head = table[index];
if (head == null) {
final TableEntry<V> insert = new TableEntry<>(key, value);
ArrayUtil.setRelease(table, index, insert);
this.addToSize(1);
return null;
}
for (TableEntry<V> curr = head;;) {
if (key == curr.key) {
if (onlyIfAbsent) {
return curr.getValuePlain();
}
final V currVal = curr.getValuePlain();
curr.setValueRelease(value);
return currVal;
}
final TableEntry<V> next = curr.getNextPlain();
if (next != null) {
curr = next;
continue;
}
final TableEntry<V> 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<V>[] table = this.getTablePlain();
final int index = (table.length - 1) & hash;
final TableEntry<V> 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<V> 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<V>[] table = this.getTablePlain();
final int index = (table.length - 1) & hash;
final TableEntry<V> 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<V> 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<? extends V> 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}
* <p>
* 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.
* </p>
*/
public void clear() {
Arrays.fill(this.getTablePlain(), null);
this.setSizeRelease(0);
}
public static final class TableEntry<V> {
protected final long key;
protected V value;
protected TableEntry<V> 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<V> getNextPlain() {
//noinspection unchecked
return (TableEntry<V>)NEXT_HANDLE.get(this);
}
protected final TableEntry<V> getNextOpaque() {
//noinspection unchecked
return (TableEntry<V>)NEXT_HANDLE.getOpaque(this);
}
protected final void setNextPlain(final TableEntry<V> next) {
NEXT_HANDLE.set(this, next);
}
protected final void setNextRelease(final TableEntry<V> 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));
}
}
}

View File

@@ -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<SchedulableTick> 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<SchedulableTick> awaiting = new LinkedSortedSet<>(TICK_COMPARATOR_BY_TIME);
private final PriorityQueue<SchedulableTick> 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}.
* <p>
* A tickable task is expected to run on a fixed interval, which is determined by
* the {@link SchedulerThreadPool}.
* </p>
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
*/
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<SchedulableTick> 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.
* <p>
* It is the callee's responsibility to invoke {@link #setScheduledStart(long)} to adjust the start of
* the next tick.
* </p>
* @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.
* <p>
* stateTarget = null
* </p>
*/
private static final int STATE_IDLE = 0;
/**
* The runner is waiting to tick a task, as it has no intermediate tasks to execute.
* <p>
* stateTarget = the task awaiting tick
* </p>
*/
private static final int STATE_AWAITING_TICK = 1;
/**
* The runner is executing a tick for one of the tasks that was in its runqueue.
* <p>
* stateTarget = the task being ticked
* </p>
*/
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);
}
}
}
}
}
}

View File

@@ -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<E> implements Iterable<E> {
public final Comparator<? super E> comparator;
protected Link<E> head;
protected Link<E> tail;
public LinkedSortedSet() {
this((Comparator)Comparator.naturalOrder());
}
public LinkedSortedSet(final Comparator<? super E> comparator) {
this.comparator = comparator;
}
public void clear() {
this.head = this.tail = null;
}
public boolean isEmpty() {
return this.head == null;
}
public E first() {
final Link<E> head = this.head;
return head == null ? null : head.element;
}
public E last() {
final Link<E> tail = this.tail;
return tail == null ? null : tail.element;
}
public boolean containsFirst(final E element) {
final Comparator<? super E> comparator = this.comparator;
for (Link<E> 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<? super E> comparator = this.comparator;
for (Link<E> curr = this.tail; curr != null; curr = curr.prev) {
if (comparator.compare(element, curr.element) == 0) {
return true;
}
}
return false;
}
private void removeNode(final Link<E> node) {
final Link<E> prev = node.prev;
final Link<E> 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<E> link) {
if (link.element == null) {
return false;
}
this.removeNode(link);
return true;
}
public boolean removeFirst(final E element) {
final Comparator<? super E> comparator = this.comparator;
for (Link<E> 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<? super E> comparator = this.comparator;
for (Link<E> 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<E> iterator() {
return new Iterator<>() {
private Link<E> next = LinkedSortedSet.this.head;
@Override
public boolean hasNext() {
return this.next != null;
}
@Override
public E next() {
final Link<E> next = this.next;
if (next == null) {
throw new NoSuchElementException();
}
this.next = next.next;
return next.element;
}
};
}
public E pollFirst() {
final Link<E> head = this.head;
if (head == null) {
return null;
}
final E ret = head.element;
final Link<E> 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<E> tail = this.tail;
if (tail == null) {
return null;
}
final E ret = tail.element;
final Link<E> 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<E> addLast(final E element) {
final Comparator<? super E> comparator = this.comparator;
Link<E> curr = this.tail;
if (curr != null) {
int compare;
while ((compare = comparator.compare(element, curr.element)) < 0) {
Link<E> 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<E> next = curr.next;
final Link<E> 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<E> addFirst(final E element) {
final Comparator<? super E> comparator = this.comparator;
Link<E> curr = this.head;
if (curr != null) {
int compare;
while ((compare = comparator.compare(element, curr.element)) > 0) {
Link<E> 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<E> prev = curr.prev;
final Link<E> 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<E> {
private E element;
private Link<E> prev;
private Link<E> next;
private Link() {}
private Link(final E element) {
this.element = element;
}
private Link(final E element, final Link<E> prev, final Link<E> next) {
this.element = element;
this.prev = prev;
this.next = next;
}
}
}

View File

@@ -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> 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> 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> 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> T getVolatile(final T[] array, final int index) {
final Object ret = OBJECT_ARRAY_HANDLE.getVolatile((Object[])array, index);
return (T)ret;
}
public static <T> void setPlain(final T[] array, final int index, final T value) {
OBJECT_ARRAY_HANDLE.set((Object[])array, index, (Object)value);
}
public static <T> void setOpaque(final T[] array, final int index, final T value) {
OBJECT_ARRAY_HANDLE.setOpaque((Object[])array, index, (Object)value);
}
public static <T> void setRelease(final T[] array, final int index, final T value) {
OBJECT_ARRAY_HANDLE.setRelease((Object[])array, index, (Object)value);
}
public static <T> void setVolatile(final T[] array, final int index, final T value) {
OBJECT_ARRAY_HANDLE.setVolatile((Object[])array, index, (Object)value);
}
public static <T> 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> 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> 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> 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> 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;
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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 <T extends Throwable> 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);
}
}

View File

@@ -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();
}
}

View File

@@ -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() {}
}

View File

@@ -0,0 +1,28 @@
package ca.spottedleaf.concurrentutil.util;
public final class Validate {
public static <T> T notNull(final T obj) {
if (obj == null) {
throw new NullPointerException();
}
return obj;
}
public static <T> 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();
}
}

View File

@@ -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<GraphNode> getDFS() {
final List<GraphNode> ret = new ArrayList<>();
final ArrayDeque<GraphNode> 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<Int2IntMap.Entry> 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);
}
}

View File

@@ -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<String, ProfilerEntry> 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) {}
}

View File

@@ -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<DecimalFormat> THREE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> {
return new DecimalFormat("#,##0.000");
});
private static final ThreadLocal<DecimalFormat> 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<ProfileNode> 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<String> dumpToString() {
final List<LProfileGraph.GraphNode> graphDFS = this.graph.getDFS();
final Reference2ReferenceOpenHashMap<LProfileGraph.GraphNode, ProfileNode> nodeMap = new Reference2ReferenceOpenHashMap<>();
final ArrayDeque<ProfileNode> 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<String> 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:
// <indent><name> X% total, Y% parent, self A% total, self B% children, avg X sum Y, Dms raw sum
// For counter type:
// <indent>#<name> 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.");
}
*/
}

View File

@@ -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<TickTime> timeData = new ArrayDeque<>();
private final SortedList<TickTime> 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<TickTime> 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<TickInformation> collapsedData = new ArrayList<>();
for (int i = 0, len = allData.size(); i < len; ++i) {
final List<TickTime> 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
) {}
}

View File

@@ -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).
*/
}

View File

@@ -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<LargeTick> 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<String> 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<String> 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<String> supplier) {
this.incrementCounter(supplier, 1);
}
@Override
public void incrementCounter(final Supplier<String> 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);
}
}

View File

@@ -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<BlockState> 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();
}
}

View File

@@ -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<E> {
public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0;
protected final Reference2IntLinkedOpenHashMap<E> 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<E> 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<E> 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<Reference2IntMap.Entry<E>> 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<E>() {
@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<E> 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<E> iterator() {
return this.iterator(0);
}
public IteratorSafeOrderedReferenceSet.Iterator<E> 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<E> unsafeIterator() {
return this.unsafeIterator(0);
}
public java.util.Iterator<E> unsafeIterator(final int flags) {
return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
}
public static interface Iterator<E> extends java.util.Iterator<E> {
public void finishedIterating();
}
protected static final class BaseIterator<E> implements IteratorSafeOrderedReferenceSet.Iterator<E> {
protected final IteratorSafeOrderedReferenceSet<E> 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<E> 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();
}
}
}

View File

@@ -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<E> implements Iterable<E> {
protected final Reference2IntOpenHashMap<E> 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<E> 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;
}
};
}
}

View File

@@ -0,0 +1,112 @@
package ca.spottedleaf.moonrise.common.list;
import java.util.Arrays;
import java.util.Comparator;
public final class SortedList<E> {
protected static final Object[] EMPTY_LIST = new Object[0];
protected Comparator<? super E> comparator;
protected Object[] elements;
protected int count;
public SortedList(final Comparator<? super E> comparator) {
this.elements = EMPTY_LIST;
this.comparator = comparator;
}
// start, end are inclusive
private static <E> int insertIdx(final E[] elements, final E element, final Comparator<E> 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<? super E> 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<? super E> 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;
}
}

View File

@@ -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];
}
}

View File

@@ -0,0 +1,74 @@
package ca.spottedleaf.moonrise.common.map;
import java.util.Arrays;
import java.util.function.IntFunction;
public class Int2ObjectArraySortedMap<V> {
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<V> 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];
}
}

View File

@@ -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];
}
}

View File

@@ -0,0 +1,76 @@
package ca.spottedleaf.moonrise.common.map;
import java.util.Arrays;
import java.util.function.LongFunction;
public class Long2ObjectArraySortedMap<V> {
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<V> 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];
}
}

View File

@@ -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();
}
}

View File

@@ -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<Ticket> 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<Ticket> empty
= new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this);
}
public static void main(final String[] args) {
com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference = new com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket>() {
@Override
protected com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<Ticket> 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<Ticket> 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<Ticket> 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;
}
}
}

View File

@@ -0,0 +1,68 @@
package ca.spottedleaf.moonrise.common.set;
import java.util.Collection;
public final class OptimizedSmallEnumSet<E extends Enum<E>> {
private final Class<E> enumClass;
private long backingSet;
public OptimizedSmallEnumSet(final Class<E> 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<E> 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<E> other) {
return (other.backingSet & this.backingSet) != 0;
}
public boolean hasElement(final E element) {
return (this.backingSet & (1L << element.ordinal())) != 0;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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 = "<init>(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);
}
}

View File

@@ -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;
}
}

View File

@@ -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<Biome> registry, long l, LevelChunkSection[] levelChunkSections, BlendingData blendingData) {
super(chunkPos, upgradeData, levelHeightAccessor, registry, l, levelChunkSections, blendingData);
}
/**
* Initialises the min/max section
*/
@Inject(
method = "<init>(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;
}
}

View File

@@ -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<Entity> RIDABLE_MINECARTS;
protected ArmorStandMixin(EntityType<? extends LivingEntity> 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<AbstractMinecart> 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);
}
}
}
}

View File

@@ -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 = "<init>(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();
}
}

View File

@@ -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();
}
}

View File

@@ -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<Block, BlockState> 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<Property<?>, Comparable<?>> immutableMap, MapCodec<BlockState> 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 = "<init>",
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;
}
}

View File

@@ -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<BlockPos> 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);
}
}

View File

@@ -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 = "<init>",
at = @At(
value = "RETURN"
)
)
private void initState(final DiscreteVoxelShape discreteVoxelShape, final CallbackInfo ci) {
((CollisionVoxelShape)this).initCache();
}
}

View File

@@ -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 = "<clinit>",
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;
}
}

View File

@@ -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
);
}
}

View File

@@ -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<Entity> getEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate);
@Shadow
List<Entity> 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<VoxelShape> 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<Entity> entities;
if (entity != null && ((CollisionEntity)entity).isHardColliding()) {
entities = this.getEntities(entity, box, null);
} else {
entities = this.getHardCollidingEntities(entity, box, null);
}
final List<VoxelShape> 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<Entity> getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> 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<Entity> 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;
}
}

View File

@@ -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<AABB> potentialCollisionsBB = new ArrayList<>();
final List<VoxelShape> 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<BlockState> 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;
}
}

View File

@@ -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<BlockState> 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<BlockState> 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;
}
}

View File

@@ -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 = "<init>",
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<Entity> getEntities(final Entity entity, final AABB boundingBox, final Predicate<? super Entity> predicate) {
this.getProfiler().incrementCounter("getEntities");
final List<Entity> ret = new ArrayList<>();
this.collisionLookup.getEntities(entity, boundingBox, ret, predicate);
return ret;
}
/**
* @reason Route to faster lookup
* @author Spottedleaf
*/
@Overwrite
public <T extends Entity> void getEntities(final EntityTypeTest<Entity, T> entityTypeTest,
final AABB boundingBox, final Predicate<? super T> predicate,
final List<? super T> into, final int maxCount) {
this.getProfiler().incrementCounter("getEntities");
if (entityTypeTest instanceof EntityType<T> 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<? extends Entity> base = entityTypeTest.getBaseClass();
final Predicate<? super T> 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 <T extends Entity> List<T> getEntitiesOfClass(final Class<T> entityClass, final AABB boundingBox, final Predicate<? super T> predicate) {
this.getProfiler().incrementCounter("getEntities");
final List<T> ret = new ArrayList<>();
this.collisionLookup.getEntities(entityClass, null, boundingBox, ret, predicate);
return ret;
}
/**
* Route to faster lookup
* @author Spottedleaf
*/
@Override
public List<Entity> getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate) {
this.getProfiler().incrementCounter("getEntities");
final List<Entity> 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<Entity> 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<BlockState> 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);
}
);
}
}

View File

@@ -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);
}
}

View File

@@ -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<Player> 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<Entity> 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);
}
}
}
}

View File

@@ -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<VoxelShape> 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<AABB> boxes = new ArrayList<>();
final List<VoxelShape> 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;
}
}

View File

@@ -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<T extends EntityAccess> {
@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);
}
}

View File

@@ -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<T extends EntityAccess> {
/**
* @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<Boolean> cir) {
final Entity entity = (Entity)entityAccess;
((CollisionLevel)entity.level()).getCollisionLookup().addEntity(entity);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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 = "<clinit>",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/Util;make(Ljava/util/function/Supplier;)Ljava/lang/Object;"
)
)
private static Object forceArrayVoxelShape(final Supplier<VoxelShape> 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();
}
}

View File

@@ -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 = "<init>",
at = @At(
value = "RETURN"
)
)
private void initState(final VoxelShape parent, final Direction.Axis forAxis, final int forIndex, final CallbackInfo ci) {
((CollisionVoxelShape)this).initCache();
}
}

View File

@@ -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<T extends EntityAccess> {
@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);
}
}

View File

@@ -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<T extends EntityAccess> {
/**
* @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);
}
}

View File

@@ -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<AABB> toAabbsUncached() {
final List<AABB> 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<AABB> 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<AABB> 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<AABB> 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;
}
}
}

View File

@@ -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<SynchedEntityData.DataItem<?>> itemsById;
@Shadow
private boolean isDirty;
@Shadow
@Final
private Entity entity;
@Shadow
protected abstract <T> void assignValue(SynchedEntityData.DataItem<T> 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 <T> void createDataItem(final EntityDataAccessor<T> entityDataAccessor, final T dfl) {
final int id = entityDataAccessor.getId();
final SynchedEntityData.DataItem<T> 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 <T> boolean hasItem(final EntityDataAccessor<T> 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 <T> SynchedEntityData.DataItem<T> getItem(final EntityDataAccessor<T> entityDataAccessor) {
final int id = entityDataAccessor.getId();
if (id < 0 || id >= this.itemsByArray.length) {
return null;
}
return (SynchedEntityData.DataItem<T>)this.itemsByArray[id];
}
/**
* @reason Remove unnecessary locking, and use the array lookup
* @author Spottedleaf
*/
@Overwrite
public List<SynchedEntityData.DataValue<?>> packDirty() {
if (!this.isDirty) {
return null;
}
this.isDirty = false;
final List<SynchedEntityData.DataValue<?>> 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<SynchedEntityData.DataValue<?>> getNonDefaultValues() {
List<SynchedEntityData.DataValue<?>> 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<SynchedEntityData.DataValue<?>> 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);
}
}

View File

@@ -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<BlockPos> toBlow;
@Shadow
public abstract DamageSource getDamageSource();
@Shadow
@Final
private Map<Player, Vec3> 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<ExplosionBlockCache> 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<Float> 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<Entity> 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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<Direction, FluidState> 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<Direction, FluidState> ret = Maps.newEnumMap(Direction.class);
final Short2ObjectOpenHashMap<BlockState> 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<Direction> 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<BlockState> 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<Direction, FluidState> spread = this.getSpread(level, fromPos, fromState);
final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos();
for (final Map.Entry<Direction, FluidState> 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));
}
}

View File

@@ -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 = "<init>",
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;
}
}

View File

@@ -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<Fluid, FluidState> implements FluidFluidState {
@Shadow
public abstract Fluid getType();
protected FluidStateMixin(Fluid object, ImmutableMap<Property<?>, Comparable<?>> immutableMap, MapCodec<FluidState> 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 = "<init>",
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;
}
}

View File

@@ -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<ItemEntity> 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<ItemEntity> 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<AABB> 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;
});
}
}

View File

@@ -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);
}
}

View File

@@ -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<Pair<Holder<PoiType>, BlockPos>> aaa(PoiManager poiManager, Predicate<Holder<PoiType>> predicate,
Predicate<BlockPos> predicate2, BlockPos blockPos, int i,
PoiManager.Occupancy occup) {
final List<Pair<Holder<PoiType>, BlockPos>> ret = new ArrayList<>();
PoiAccess.findNearestPoiPositions(
poiManager, predicate, predicate2, blockPos, i, Double.MAX_VALUE, occup, true, 5, ret
);
return ret.stream();
}
}

View File

@@ -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<PoiSection> {
public PoiManagerMixin(Path path, Function<Runnable, Codec<PoiSection>> function, Function<Runnable, PoiSection> 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<BlockPos> find(Predicate<Holder<PoiType>> typePredicate, Predicate<BlockPos> 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<BlockPos> findClosest(Predicate<Holder<PoiType>> 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<Pair<Holder<PoiType>, BlockPos>> findClosestWithType(Predicate<Holder<PoiType>> 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<BlockPos> findClosest(Predicate<Holder<PoiType>> typePredicate, Predicate<BlockPos> 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<BlockPos> take(Predicate<Holder<PoiType>> typePredicate, BiPredicate<Holder<PoiType>, 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<BlockPos> getRandom(Predicate<Holder<PoiType>> typePredicate, Predicate<BlockPos> positionPredicate,
PoiManager.Occupancy occupationStatus, BlockPos pos, int radius, RandomSource random) {
List<PoiRecord> 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<Pair<Holder<PoiType>, BlockPos>> findAllWithType(Predicate<Holder<PoiType>> predicate, Predicate<BlockPos> predicate2,
BlockPos blockPos, int i, PoiManager.Occupancy occupancy) {
List<Pair<Holder<PoiType>, 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<Pair<Holder<PoiType>, BlockPos>> findAllClosestFirstWithType(Predicate<Holder<PoiType>> predicate,
Predicate<BlockPos> predicate2, BlockPos blockPos,
int i, PoiManager.Occupancy occupancy) {
List<Pair<Holder<PoiType>, 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();
}
}

View File

@@ -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<BlockUtil.FoundRectangle> findPortalAround(BlockPos blockPos, boolean bl, WorldBorder worldBorder) {
PoiManager poiManager = this.level.getPoiManager();
int i = bl ? 16 : 128;
List<PoiRecord> 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;
}));
}
}

View File

@@ -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<Runnable> 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;
}
}

View File

@@ -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<Block, BlockState> 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<Property<?>, Comparable<?>> immutableMap, final MapCodec<BlockState> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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) {}
}

View File

@@ -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);
}
}

View File

@@ -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 = "<init>(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 = "<init>(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));
}
}

View File

@@ -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 = "<init>(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));
}
}

View File

@@ -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 = "<init>", 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<SWMRNibbleArray[]> blockLightMap = new Long2ObjectOpenHashMap<>();
@Unique
protected final Long2ObjectOpenHashMap<SWMRNibbleArray[]> 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);
}
}
}

View File

@@ -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<StarLightInterface.LightQueue.ChunkTasks> 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<ChunkAccess> 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<ChunkAccess> 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);
}
});
}
}

View File

@@ -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);
}
}

View File

@@ -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<CompoundTag> 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<ProtoChunk> cir) {
SaveUtil.loadLightHook(serverLevel, chunkPos, compoundTag, cir.getReturnValue());
}
}

Some files were not shown because too many files have changed in this diff Show More