Initial Commit
This commit is contained in:
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
patreon: Spottedleaf
|
||||
118
.gitignore
vendored
Normal file
118
.gitignore
vendored
Normal 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
675
LICENSE.md
Normal 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
97
build.gradle
Normal 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
12
gradle.properties
Normal 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
240
gradlew
vendored
Normal 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
91
gradlew.bat
vendored
Normal 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
10
settings.gradle
Normal 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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
1673
src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java
Normal file
1673
src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
816
src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java
Normal file
816
src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
107
src/main/java/ca/spottedleaf/leafprofiler/LProfileGraph.java
Normal file
107
src/main/java/ca/spottedleaf/leafprofiler/LProfileGraph.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
382
src/main/java/ca/spottedleaf/leafprofiler/LeafProfiler.java
Normal file
382
src/main/java/ca/spottedleaf/leafprofiler/LeafProfiler.java
Normal 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.");
|
||||
}
|
||||
*/
|
||||
}
|
||||
341
src/main/java/ca/spottedleaf/leafprofiler/TickAccumulator.java
Normal file
341
src/main/java/ca/spottedleaf/leafprofiler/TickAccumulator.java
Normal 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
|
||||
) {}
|
||||
}
|
||||
67
src/main/java/ca/spottedleaf/leafprofiler/TickTime.java
Normal file
67
src/main/java/ca/spottedleaf/leafprofiler/TickTime.java
Normal 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).
|
||||
*/
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user