diff --git a/paper-server/.gitignore b/paper-server/.gitignore
new file mode 100644
index 0000000000..3811c0d849
--- /dev/null
+++ b/paper-server/.gitignore
@@ -0,0 +1,48 @@
+.gradle/
+build/
+
+# Eclipse stuff
+/.classpath
+/.project
+/.settings
+/.checkstyle
+/.factorypath
+
+# netbeans
+/nbproject
+nb*.xml
+
+# we use maven!
+/build.xml
+
+# maven
+/target
+dependency-reduced-pom.xml
+
+# vim
+.*.sw[a-p]
+
+# various other potential build files
+/build
+/bin
+/dist
+/manifest.mf
+
+/world
+/logs
+
+# Mac filesystem dust
+.DS_Store
+
+# intellij
+*.iml
+*.ipr
+*.iws
+.idea/
+
+/src/main/resources/achievement
+/src/main/resources/lang
+
+# vs code
+/.vscode
+/.factorypath
diff --git a/paper-server/LGPL.txt b/paper-server/LGPL.txt
new file mode 100644
index 0000000000..65c5ca88a6
--- /dev/null
+++ b/paper-server/LGPL.txt
@@ -0,0 +1,165 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser 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
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/paper-server/LICENCE.txt b/paper-server/LICENCE.txt
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/paper-server/LICENCE.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/paper-server/build.gradle.kts b/paper-server/build.gradle.kts
new file mode 100644
index 0000000000..c056b0fa1b
--- /dev/null
+++ b/paper-server/build.gradle.kts
@@ -0,0 +1,249 @@
+import io.papermc.paperweight.util.*
+import java.time.Instant
+
+plugins {
+ java
+ `maven-publish`
+}
+
+val log4jPlugins = sourceSets.create("log4jPlugins")
+configurations.named(log4jPlugins.compileClasspathConfigurationName) {
+ extendsFrom(configurations.compileClasspath.get())
+}
+val alsoShade: Configuration by configurations.creating
+
+// Paper start - configure mockito agent that is needed in newer java versions
+val mockitoAgent = configurations.register("mockitoAgent")
+abstract class MockitoAgentProvider : CommandLineArgumentProvider {
+ @get:CompileClasspath
+ abstract val fileCollection: ConfigurableFileCollection
+
+ override fun asArguments(): Iterable {
+ return listOf("-javaagent:" + fileCollection.files.single().absolutePath)
+ }
+}
+// Paper end - configure mockito agent that is needed in newer java versions
+
+dependencies {
+ implementation(project(":paper-api"))
+ implementation("ca.spottedleaf:concurrentutil:0.0.2") // Paper - Add ConcurrentUtil dependency
+ // Paper start
+ implementation("org.jline:jline-terminal-ffm:3.27.1") // use ffm on java 22+
+ implementation("org.jline:jline-terminal-jni:3.27.1") // fall back to jni on java 21
+ implementation("net.minecrell:terminalconsoleappender:1.3.0")
+ implementation("net.kyori:adventure-text-serializer-ansi:4.17.0") // Keep in sync with adventureVersion from Paper-API build file
+ /*
+ Required to add the missing Log4j2Plugins.dat file from log4j-core
+ which has been removed by Mojang. Without it, log4j has to classload
+ all its classes to check if they are plugins.
+ Scanning takes about 1-2 seconds so adding this speeds up the server start.
+ */
+ implementation("org.apache.logging.log4j:log4j-core:2.19.0") // Paper - implementation
+ log4jPlugins.annotationProcessorConfigurationName("org.apache.logging.log4j:log4j-core:2.19.0") // Paper - Needed to generate meta for our Log4j plugins
+ runtimeOnly(log4jPlugins.output)
+ alsoShade(log4jPlugins.output)
+ implementation("io.netty:netty-codec-haproxy:4.1.97.Final") // Paper - Add support for proxy protocol
+ // Paper end
+ implementation("org.apache.logging.log4j:log4j-iostreams:2.24.1") // Paper - remove exclusion
+ implementation("org.ow2.asm:asm-commons:9.7.1")
+ implementation("org.spongepowered:configurate-yaml:4.2.0-SNAPSHOT") // Paper - config files
+ implementation("commons-lang:commons-lang:2.6")
+ runtimeOnly("org.xerial:sqlite-jdbc:3.47.0.0")
+ runtimeOnly("com.mysql:mysql-connector-j:9.1.0")
+ runtimeOnly("com.lmax:disruptor:3.4.4") // Paper
+
+ runtimeOnly("org.apache.maven:maven-resolver-provider:3.9.6")
+ runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.18")
+ runtimeOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.9.18")
+
+ testImplementation("io.github.classgraph:classgraph:4.8.47") // Paper - mob goal test
+ testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
+ testImplementation("org.junit.platform:junit-platform-suite-engine:1.10.0")
+ testImplementation("org.hamcrest:hamcrest:2.2")
+ testImplementation("org.mockito:mockito-core:5.14.1")
+ mockitoAgent("org.mockito:mockito-core:5.14.1") { isTransitive = false } // Paper - configure mockito agent that is needed in newer java versions
+ testImplementation("org.ow2.asm:asm-tree:9.7.1")
+ testImplementation("org.junit-pioneer:junit-pioneer:2.2.0") // Paper - CartesianTest
+ implementation("net.neoforged:srgutils:1.0.9") // Paper - mappings handling
+ implementation("net.neoforged:AutoRenamingTool:2.0.3") // Paper - remap plugins
+ // Paper start - Remap reflection
+ val reflectionRewriterVersion = "0.0.3"
+ implementation("io.papermc:reflection-rewriter:$reflectionRewriterVersion")
+ implementation("io.papermc:reflection-rewriter-runtime:$reflectionRewriterVersion")
+ implementation("io.papermc:reflection-rewriter-proxy-generator:$reflectionRewriterVersion")
+ // Paper end - Remap reflection
+ // Paper start - spark
+ implementation("me.lucko:spark-api:0.1-20240720.200737-2")
+ implementation("me.lucko:spark-paper:1.10.119-SNAPSHOT")
+ // Paper end - spark
+}
+
+paperweight {
+ craftBukkitPackageVersion.set("v1_21_R3") // also needs to be updated in MappingEnvironment
+}
+
+tasks.jar {
+ archiveClassifier.set("dev")
+
+ manifest {
+ val git = Git(rootProject.layout.projectDirectory.path)
+ val mcVersion = rootProject.providers.gradleProperty("mcVersion").get()
+ val build = System.getenv("BUILD_NUMBER") ?: null
+ val gitHash = git("rev-parse", "--short=7", "HEAD").getText().trim()
+ val implementationVersion = "$mcVersion-${build ?: "DEV"}-$gitHash"
+ val date = git("show", "-s", "--format=%ci", gitHash).getText().trim() // Paper
+ val gitBranch = git("rev-parse", "--abbrev-ref", "HEAD").getText().trim() // Paper
+ attributes(
+ "Main-Class" to "org.bukkit.craftbukkit.Main",
+ "Implementation-Title" to "Paper",
+ "Implementation-Version" to implementationVersion,
+ "Implementation-Vendor" to date, // Paper
+ "Specification-Title" to "Paper",
+ "Specification-Version" to project.version,
+ "Specification-Vendor" to "Paper Team",
+ "Brand-Id" to "papermc:paper",
+ "Brand-Name" to "Paper",
+ "Build-Number" to (build ?: ""),
+ "Build-Time" to Instant.now().toString(),
+ "Git-Branch" to gitBranch, // Paper
+ "Git-Commit" to gitHash, // Paper
+ "CraftBukkit-Package-Version" to paperweight.craftBukkitPackageVersion.get(), // Paper
+ )
+ for (tld in setOf("net", "com", "org")) {
+ attributes("$tld/bukkit", "Sealed" to true)
+ }
+ }
+}
+
+// Paper start - compile tests with -parameters for better junit parameterized test names
+tasks.compileTestJava {
+ options.compilerArgs.add("-parameters")
+}
+// Paper end
+
+publishing {
+ publications.create("maven") {
+ }
+}
+
+// Paper start
+val scanJar = tasks.register("scanJarForBadCalls", io.papermc.paperweight.tasks.ScanJarForBadCalls::class) {
+ badAnnotations.add("Lio/papermc/paper/annotation/DoNotUse;")
+ jarToScan.set(tasks.serverJar.flatMap { it.archiveFile })
+ classpath.from(configurations.compileClasspath)
+}
+tasks.check {
+ dependsOn(scanJar)
+}
+// Paper end
+// Paper start - use TCA for console improvements
+tasks.serverJar {
+ from(alsoShade.elements.map {
+ it.map { f ->
+ if (f.asFile.isFile) {
+ zipTree(f.asFile)
+ } else {
+ f.asFile
+ }
+ }
+ })
+}
+// Paper end - use TCA for console improvements
+
+tasks.test {
+ include("**/**TestSuite.class")
+ workingDir = temporaryDir
+ useJUnitPlatform {
+ forkEvery = 1
+ excludeTags("Slow")
+ }
+ // Paper start - configure mockito agent that is needed in newer java versions
+ val provider = objects.newInstance()
+ provider.fileCollection.from(mockitoAgent)
+ jvmArgumentProviders.add(provider)
+ // Paper end - configure mockito agent that is needed in newer java versions
+}
+
+fun TaskContainer.registerRunTask(
+ name: String,
+ block: JavaExec.() -> Unit
+): TaskProvider = register(name) {
+ group = "paper"
+ mainClass.set("org.bukkit.craftbukkit.Main")
+ standardInput = System.`in`
+ workingDir = rootProject.layout.projectDirectory
+ .dir(providers.gradleProperty("paper.runWorkDir").getOrElse("run"))
+ .asFile
+ javaLauncher.set(project.javaToolchains.launcherFor {
+ languageVersion.set(JavaLanguageVersion.of(21))
+ vendor.set(JvmVendorSpec.JETBRAINS)
+ })
+ jvmArgs("-XX:+AllowEnhancedClassRedefinition", "-XX:+AllowRedefinitionToAddDeleteMethods")
+
+ if (rootProject.childProjects["test-plugin"] != null) {
+ val testPluginJar = rootProject.project(":test-plugin").tasks.jar.flatMap { it.archiveFile }
+ inputs.file(testPluginJar)
+ args("-add-plugin=${testPluginJar.get().asFile.absolutePath}")
+ }
+
+ args("--nogui")
+ systemProperty("net.kyori.adventure.text.warnWhenLegacyFormattingDetected", true)
+ if (providers.gradleProperty("paper.runDisableWatchdog").getOrElse("false") == "true") {
+ systemProperty("disable.watchdog", true)
+ }
+ systemProperty("io.papermc.paper.suppress.sout.nags", true)
+
+ val memoryGb = providers.gradleProperty("paper.runMemoryGb").getOrElse("2")
+ minHeapSize = "${memoryGb}G"
+ maxHeapSize = "${memoryGb}G"
+
+ doFirst {
+ workingDir.mkdirs()
+ }
+
+ block(this)
+}
+
+val runtimeClasspathWithoutVanillaServer = configurations.runtimeClasspath.flatMap { it.elements }
+ .zip(configurations.vanillaServer.map { it.singleFile.absolutePath }) { runtime, vanilla ->
+ runtime.filterNot { it.asFile.absolutePath == vanilla }
+ }
+
+tasks.registerRunTask("runServer") {
+ description = "Spin up a test server from the Mojang mapped server jar"
+ classpath(tasks.includeMappings.flatMap { it.outputJar })
+ classpath(runtimeClasspathWithoutVanillaServer)
+}
+
+tasks.registerRunTask("runReobfServer") {
+ description = "Spin up a test server from the reobfJar output jar"
+ classpath(tasks.reobfJar.flatMap { it.outputJar })
+ classpath(runtimeClasspathWithoutVanillaServer)
+}
+
+tasks.registerRunTask("runDevServer") {
+ description = "Spin up a test server without assembling a jar"
+ classpath(sourceSets.main.map { it.runtimeClasspath })
+ jvmArgs("-DPaper.pushPaperAssetsRoot=true")
+}
+
+tasks.registerRunTask("runBundler") {
+ description = "Spin up a test server from the Mojang mapped bundler jar"
+ classpath(rootProject.tasks.named("createMojmapBundlerJar").flatMap { it.outputZip })
+ mainClass.set(null as String?)
+}
+tasks.registerRunTask("runReobfBundler") {
+ description = "Spin up a test server from the reobf bundler jar"
+ classpath(rootProject.tasks.named("createReobfBundlerJar").flatMap { it.outputZip })
+ mainClass.set(null as String?)
+}
+tasks.registerRunTask("runPaperclip") {
+ description = "Spin up a test server from the Mojang mapped Paperclip jar"
+ classpath(rootProject.tasks.named("createMojmapPaperclipJar").flatMap { it.outputZip })
+ mainClass.set(null as String?)
+}
+tasks.registerRunTask("runReobfPaperclip") {
+ description = "Spin up a test server from the reobf Paperclip jar"
+ classpath(rootProject.tasks.named("createReobfPaperclipJar").flatMap { it.outputZip })
+ mainClass.set(null as String?)
+}
diff --git a/paper-server/patches/resources/data/minecraft/loot_table/chests/trial_chambers/intersection_barrel.json.patch b/paper-server/patches/resources/data/minecraft/loot_table/chests/trial_chambers/intersection_barrel.json.patch
new file mode 100644
index 0000000000..572f83673b
--- /dev/null
+++ b/paper-server/patches/resources/data/minecraft/loot_table/chests/trial_chambers/intersection_barrel.json.patch
@@ -0,0 +1,18 @@
+--- a/data/minecraft/loot_table/chests/trial_chambers/intersection_barrel.json
++++ b/data/minecraft/loot_table/chests/trial_chambers/intersection_barrel.json
+@@ -70,15 +70,6 @@
+ "add": false,
+ "count": 1.0,
+ "function": "minecraft:set_count"
+- },
+- {
+- "add": false,
+- "damage": {
+- "type": "minecraft:uniform",
+- "max": 0.8,
+- "min": 0.15
+- },
+- "function": "minecraft:set_damage"
+ }
+ ],
+ "name": "minecraft:compass"
diff --git a/paper-server/patches/sources/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java.patch b/paper-server/patches/sources/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java.patch
new file mode 100644
index 0000000000..30f05bd5aa
--- /dev/null
+++ b/paper-server/patches/sources/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java.patch
@@ -0,0 +1,23 @@
+--- a/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java
++++ b/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java
+@@ -44,6 +44,7 @@
+ .collect(Collectors.toSet());
+
+ final int page = 0;
++ boolean hasRequested = false; // Paper - Don't sleep after profile lookups if not needed
+
+ for (final List request : Iterables.partition(criteria, ENTRIES_PER_PAGE)) {
+ final List normalizedRequest = request.stream().map(YggdrasilGameProfileRepository::normalizeName).toList();
+@@ -75,6 +76,12 @@
+ LOGGER.debug("Couldn't find profile {}", name);
+ callback.onProfileLookupFailed(name, new ProfileNotFoundException("Server did not find the requested profile"));
+ }
++ // Paper start - Don't sleep after profile lookups if not needed
++ if (!hasRequested) {
++ hasRequested = true;
++ continue;
++ }
++ // Paper end - Don't sleep after profile lookups if not needed
+
+ try {
+ Thread.sleep(DELAY_BETWEEN_PAGES);
diff --git a/paper-server/patches/sources/com/mojang/brigadier/CommandDispatcher.java.patch b/paper-server/patches/sources/com/mojang/brigadier/CommandDispatcher.java.patch
new file mode 100644
index 0000000000..ba95e196e5
--- /dev/null
+++ b/paper-server/patches/sources/com/mojang/brigadier/CommandDispatcher.java.patch
@@ -0,0 +1,75 @@
+--- a/com/mojang/brigadier/CommandDispatcher.java
++++ b/com/mojang/brigadier/CommandDispatcher.java
+@@ -3,6 +3,7 @@
+
+ package com.mojang.brigadier;
+
++// CHECKSTYLE:OFF
+ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+ import com.mojang.brigadier.context.CommandContext;
+ import com.mojang.brigadier.context.CommandContextBuilder;
+@@ -297,15 +298,21 @@
+ List> potentials = null;
+ final int cursor = originalReader.getCursor();
+
+- for (final CommandNode child : node.getRelevantNodes(originalReader)) {
++ for (final CommandNode child : node.getRelevantNodes(originalReader, source)) { // Paper - prioritize mc commands in function parsing
+ if (!child.canUse(source)) {
+ continue;
+ }
+ final CommandContextBuilder context = contextSoFar.copy();
+ final StringReader reader = new StringReader(originalReader);
++ boolean stop = false; // Paper - Handle non-recoverable exceptions
+ try {
+ try {
+ child.parse(reader, context);
++ // Paper start - Handle non-recoverable exceptions
++ } catch (final io.papermc.paper.brigadier.TagParseCommandSyntaxException e) {
++ stop = true;
++ throw e;
++ // Paper end - Handle non-recoverable exceptions
+ } catch (final RuntimeException ex) {
+ throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherParseException().createWithContext(reader, ex.getMessage());
+ }
+@@ -320,6 +327,7 @@
+ }
+ errors.put(child, ex);
+ reader.setCursor(cursor);
++ if (stop) return new ParseResults<>(contextSoFar, originalReader, errors); // Paper - Handle non-recoverable exceptions
+ continue;
+ }
+
+@@ -451,7 +459,7 @@
+ }
+
+ private String getSmartUsage(final CommandNode node, final S source, final boolean optional, final boolean deep) {
+- if (!node.canUse(source)) {
++ if (source != null && !node.canUse(source)) { // Paper
+ return null;
+ }
+
+@@ -465,7 +473,7 @@
+ final String redirect = node.getRedirect() == this.root ? "..." : "-> " + node.getRedirect().getUsageText();
+ return self + CommandDispatcher.ARGUMENT_SEPARATOR + redirect;
+ } else {
+- final Collection> children = node.getChildren().stream().filter(c -> c.canUse(source)).collect(Collectors.toList());
++ final Collection> children = node.getChildren().stream().filter(c -> source == null || c.canUse(source)).collect(Collectors.toList()); // Paper
+ if (children.size() == 1) {
+ final String usage = this.getSmartUsage(children.iterator().next(), source, childOptional, childOptional);
+ if (usage != null) {
+@@ -537,10 +545,14 @@
+ int i = 0;
+ for (final CommandNode node : parent.getChildren()) {
+ CompletableFuture future = Suggestions.empty();
++ // Paper start - Don't suggest if the requirement isn't met
++ if (parent != this.root || node.canUse(context.getSource())) {
+ try {
+- future = node.listSuggestions(context.build(truncatedInput), new SuggestionsBuilder(truncatedInput, truncatedInputLowerCase, start));
++ future = node.listSuggestions(context.build(truncatedInput), new SuggestionsBuilder(truncatedInput, truncatedInputLowerCase, start)); // CraftBukkit
+ } catch (final CommandSyntaxException ignored) {
+ }
++ }
++ // Paper end - Don't suggest if the requirement isn't met
+ futures[i++] = future;
+ }
+
diff --git a/paper-server/patches/sources/com/mojang/brigadier/builder/ArgumentBuilder.java.patch b/paper-server/patches/sources/com/mojang/brigadier/builder/ArgumentBuilder.java.patch
new file mode 100644
index 0000000000..16daaa3a3c
--- /dev/null
+++ b/paper-server/patches/sources/com/mojang/brigadier/builder/ArgumentBuilder.java.patch
@@ -0,0 +1,21 @@
+--- a/com/mojang/brigadier/builder/ArgumentBuilder.java
++++ b/com/mojang/brigadier/builder/ArgumentBuilder.java
+@@ -14,9 +14,17 @@
+ import java.util.function.Predicate;
+
+ public abstract class ArgumentBuilder> {
++ // Paper start - Vanilla command permission fixes
++ private static final Predicate DEFAULT_REQUIREMENT = s -> true;
++
++ @SuppressWarnings("unchecked")
++ public static Predicate defaultRequirement() {
++ return (Predicate) DEFAULT_REQUIREMENT;
++ }
++ // Paper end - Vanilla command permission fixes
+ private final RootCommandNode arguments = new RootCommandNode<>();
+ private Command command;
+- private Predicate requirement = s -> true;
++ private Predicate requirement = defaultRequirement(); // Paper - Vanilla command permission fixes
+ private CommandNode target;
+ private RedirectModifier modifier = null;
+ private boolean forks;
diff --git a/paper-server/patches/sources/com/mojang/brigadier/exceptions/CommandSyntaxException.java.patch b/paper-server/patches/sources/com/mojang/brigadier/exceptions/CommandSyntaxException.java.patch
new file mode 100644
index 0000000000..b3bbcc46e5
--- /dev/null
+++ b/paper-server/patches/sources/com/mojang/brigadier/exceptions/CommandSyntaxException.java.patch
@@ -0,0 +1,23 @@
+--- a/com/mojang/brigadier/exceptions/CommandSyntaxException.java
++++ b/com/mojang/brigadier/exceptions/CommandSyntaxException.java
+@@ -5,7 +5,7 @@
+
+ import com.mojang.brigadier.Message;
+
+-public class CommandSyntaxException extends Exception {
++public class CommandSyntaxException extends Exception implements net.kyori.adventure.util.ComponentMessageThrowable { // Paper - Brigadier API
+ public static final int CONTEXT_AMOUNT = 10;
+ public static boolean ENABLE_COMMAND_STACK_TRACES = true;
+ public static BuiltInExceptionProvider BUILT_IN_EXCEPTIONS = new BuiltInExceptions();
+@@ -73,4 +73,11 @@
+ public int getCursor() {
+ return cursor;
+ }
++
++ // Paper start - Brigadier API
++ @Override
++ public @org.jetbrains.annotations.Nullable net.kyori.adventure.text.Component componentMessage() {
++ return io.papermc.paper.brigadier.PaperBrigadier.componentFromMessage(this.message);
++ }
++ // Paper end - Brigadier API
+ }
diff --git a/paper-server/patches/sources/com/mojang/brigadier/tree/CommandNode.java.patch b/paper-server/patches/sources/com/mojang/brigadier/tree/CommandNode.java.patch
new file mode 100644
index 0000000000..179686de6f
--- /dev/null
+++ b/paper-server/patches/sources/com/mojang/brigadier/tree/CommandNode.java.patch
@@ -0,0 +1,102 @@
+--- a/com/mojang/brigadier/tree/CommandNode.java
++++ b/com/mojang/brigadier/tree/CommandNode.java
+@@ -3,6 +3,7 @@
+
+ package com.mojang.brigadier.tree;
+
++// CHECKSTYLE:OFF
+ import com.mojang.brigadier.AmbiguityConsumer;
+ import com.mojang.brigadier.Command;
+ import com.mojang.brigadier.RedirectModifier;
+@@ -22,6 +23,7 @@
+ import java.util.Set;
+ import java.util.concurrent.CompletableFuture;
+ import java.util.function.Predicate;
++import net.minecraft.commands.CommandSourceStack;
+
+ public abstract class CommandNode implements Comparable> {
+ private final Map> children = new LinkedHashMap<>();
+@@ -32,6 +34,16 @@
+ private final RedirectModifier modifier;
+ private final boolean forks;
+ private Command command;
++ public CommandNode clientNode; // Paper - Brigadier API
++ public CommandNode unwrappedCached = null; // Paper - Brigadier Command API
++ public CommandNode wrappedCached = null; // Paper - Brigadier Command API
++ // CraftBukkit start
++ public void removeCommand(String name) {
++ this.children.remove(name);
++ this.literals.remove(name);
++ this.arguments.remove(name);
++ }
++ // CraftBukkit end
+
+ protected CommandNode(final Command command, final Predicate requirement, final CommandNode redirect, final RedirectModifier modifier, final boolean forks) {
+ this.command = command;
+@@ -61,7 +73,17 @@
+ return this.modifier;
+ }
+
+- public boolean canUse(final S source) {
++ // CraftBukkit start
++ public synchronized boolean canUse(final S source) {
++ if (source instanceof CommandSourceStack) {
++ try {
++ ((CommandSourceStack) source).currentCommand.put(Thread.currentThread(), this); // Paper - Thread Safe Vanilla Command permission checking
++ return this.requirement.test(source);
++ } finally {
++ ((CommandSourceStack) source).currentCommand.remove(Thread.currentThread()); // Paper - Thread Safe Vanilla Command permission checking
++ }
++ }
++ // CraftBukkit end
+ return this.requirement.test(source);
+ }
+
+@@ -151,6 +173,12 @@
+ protected abstract String getSortedKey();
+
+ public Collection extends CommandNode> getRelevantNodes(final StringReader input) {
++ // Paper start - prioritize mc commands in function parsing
++ return this.getRelevantNodes(input, null);
++ }
++ @org.jetbrains.annotations.ApiStatus.Internal
++ public Collection extends CommandNode> getRelevantNodes(final StringReader input, final Object source) {
++ // Paper end - prioritize mc commands in function parsing
+ if (this.literals.size() > 0) {
+ final int cursor = input.getCursor();
+ while (input.canRead() && input.peek() != ' ') {
+@@ -158,7 +186,21 @@
+ }
+ final String text = input.getString().substring(cursor, input.getCursor());
+ input.setCursor(cursor);
+- final LiteralCommandNode literal = this.literals.get(text);
++ // Paper start - prioritize mc commands in function parsing
++ LiteralCommandNode literal = null;
++ if (source instanceof CommandSourceStack css && css.source == net.minecraft.commands.CommandSource.NULL) {
++ if (!text.contains(":")) {
++ literal = this.literals.get("minecraft:" + text);
++ }
++ } else if (source instanceof CommandSourceStack css && css.source instanceof net.minecraft.world.level.BaseCommandBlock) {
++ if (css.getServer().server.getCommandBlockOverride(text) && !text.contains(":")) {
++ literal = this.literals.get("minecraft:" + text);
++ }
++ }
++ if (literal == null) {
++ literal = this.literals.get(text);
++ }
++ // Paper end - prioritize mc commands in function parsing
+ if (literal != null) {
+ return Collections.singleton(literal);
+ } else {
+@@ -183,4 +225,11 @@
+ }
+
+ public abstract Collection getExamples();
++ // Paper start - Brigadier Command API
++ public void clearAll() {
++ this.children.clear();
++ this.literals.clear();
++ this.arguments.clear();
++ }
++ // Paper end - Brigadier Command API
+ }
diff --git a/paper-server/patches/sources/com/mojang/brigadier/tree/LiteralCommandNode.java.patch b/paper-server/patches/sources/com/mojang/brigadier/tree/LiteralCommandNode.java.patch
new file mode 100644
index 0000000000..15d0b7b976
--- /dev/null
+++ b/paper-server/patches/sources/com/mojang/brigadier/tree/LiteralCommandNode.java.patch
@@ -0,0 +1,57 @@
+--- a/com/mojang/brigadier/tree/LiteralCommandNode.java
++++ b/com/mojang/brigadier/tree/LiteralCommandNode.java
+@@ -23,11 +23,19 @@
+ public class LiteralCommandNode extends CommandNode {
+ private final String literal;
+ private final String literalLowerCase;
++ private final String nonPrefixed; // Paper - prioritize mc commands in function parsing
+
+ public LiteralCommandNode(final String literal, final Command command, final Predicate requirement, final CommandNode redirect, final RedirectModifier modifier, final boolean forks) {
+ super(command, requirement, redirect, modifier, forks);
+ this.literal = literal;
+ this.literalLowerCase = literal.toLowerCase(Locale.ROOT);
++ // Paper start - prioritize mc commands in function parsing
++ if (literal.startsWith("minecraft:")) {
++ this.nonPrefixed = literal.substring("minecraft:".length());
++ } else {
++ this.nonPrefixed = null;
++ }
++ // Paper end - prioritize mc commands in function parsing
+ }
+
+ public String getLiteral() {
+@@ -42,7 +50,12 @@
+ @Override
+ public void parse(final StringReader reader, final CommandContextBuilder contextBuilder) throws CommandSyntaxException {
+ final int start = reader.getCursor();
+- final int end = parse(reader);
++ // Paper start - prioritize mc commands in function parsing
++ int end = parse(reader, false);
++ if (end == -1 && this.nonPrefixed != null) {
++ end = parse(reader, true);
++ }
++ // Paper end - prioritize mc commands in function parsing
+ if (end > -1) {
+ contextBuilder.withNode(this, StringRange.between(start, end));
+ return;
+@@ -51,7 +64,10 @@
+ throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.literalIncorrect().createWithContext(reader, literal);
+ }
+
+- private int parse(final StringReader reader) {
++ // Paper start - prioritize mc commands in function parsing
++ private int parse(final StringReader reader, final boolean secondPass) {
++ String literal = secondPass ? this.nonPrefixed : this.literal;
++ // Paper end - prioritize mc commands in function parsing
+ final int start = reader.getCursor();
+ if (reader.canRead(literal.length())) {
+ final int end = start + literal.length();
+@@ -78,7 +94,7 @@
+
+ @Override
+ public boolean isValidInput(final String input) {
+- return parse(new StringReader(input)) > -1;
++ return parse(new StringReader(input), false) > -1; // Paper - prioritize mc commands in function parsing
+ }
+
+ @Override
diff --git a/paper-server/patches/sources/com/mojang/datafixers/DataFixerBuilder.java.patch b/paper-server/patches/sources/com/mojang/datafixers/DataFixerBuilder.java.patch
new file mode 100644
index 0000000000..6d1be183b0
--- /dev/null
+++ b/paper-server/patches/sources/com/mojang/datafixers/DataFixerBuilder.java.patch
@@ -0,0 +1,21 @@
+--- a/com/mojang/datafixers/DataFixerBuilder.java
++++ b/com/mojang/datafixers/DataFixerBuilder.java
+@@ -29,8 +29,10 @@
+ private final Int2ObjectSortedMap schemas = new Int2ObjectAVLTreeMap<>();
+ private final List globalList = new ArrayList<>();
+ private final IntSortedSet fixerVersions = new IntAVLTreeSet();
++ private final int minDataFixPrecacheVersion; // Paper - Perf: Cache DataFixerUpper Rewrite Rules on demand
+
+ public DataFixerBuilder(final int dataVersion) {
++ minDataFixPrecacheVersion = Integer.getInteger("Paper.minPrecachedDatafixVersion", dataVersion+1) * 10; // Paper - Perf: default to precache nothing - mojang stores versions * 10 to allow for 'sub versions'
+ this.dataVersion = dataVersion;
+ }
+
+@@ -88,6 +90,7 @@
+ final IntIterator iterator = fixerUpper.fixerVersions().iterator();
+ while (iterator.hasNext()) {
+ final int versionKey = iterator.nextInt();
++ if (versionKey < minDataFixPrecacheVersion) continue; // Paper - Perf: Cache DataFixerUpper Rewrite Rules on demand
+ final Schema schema = schemas.get(versionKey);
+ for (final String typeName : schema.types()) {
+ if (!requiredTypeNames.contains(typeName)) {
diff --git a/paper-server/patches/sources/com/mojang/datafixers/util/Either.java.patch b/paper-server/patches/sources/com/mojang/datafixers/util/Either.java.patch
new file mode 100644
index 0000000000..810603f8a3
--- /dev/null
+++ b/paper-server/patches/sources/com/mojang/datafixers/util/Either.java.patch
@@ -0,0 +1,38 @@
+--- a/com/mojang/datafixers/util/Either.java
++++ b/com/mojang/datafixers/util/Either.java
+@@ -22,7 +22,7 @@
+ }
+
+ private static final class Left extends Either {
+- private final L value;
++ private final L value; private Optional valueOptional; // Paper - Perf: Reduce Either Optional allocation
+
+ public Left(final L value) {
+ this.value = value;
+@@ -51,7 +51,7 @@
+
+ @Override
+ public Optional left() {
+- return Optional.of(value);
++ return this.valueOptional == null ? this.valueOptional = Optional.of(this.value) : this.valueOptional; // Paper - Perf: Reduce Either Optional allocation
+ }
+
+ @Override
+@@ -83,7 +83,7 @@
+ }
+
+ private static final class Right extends Either {
+- private final R value;
++ private final R value; private Optional valueOptional; // Paper - Perf: Reduce Either Optional allocation
+
+ public Right(final R value) {
+ this.value = value;
+@@ -117,7 +117,7 @@
+
+ @Override
+ public Optional right() {
+- return Optional.of(value);
++ return this.valueOptional == null ? this.valueOptional = Optional.of(this.value) : this.valueOptional; // Paper - Perf: Reduce Either Optional allocation
+ }
+
+ @Override
diff --git a/paper-server/patches/sources/com/mojang/logging/LogUtils.java.patch b/paper-server/patches/sources/com/mojang/logging/LogUtils.java.patch
new file mode 100644
index 0000000000..1cabed3cc3
--- /dev/null
+++ b/paper-server/patches/sources/com/mojang/logging/LogUtils.java.patch
@@ -0,0 +1,12 @@
+--- a/com/mojang/logging/LogUtils.java
++++ b/com/mojang/logging/LogUtils.java
+@@ -61,4 +61,9 @@
+ public static Logger getLogger() {
+ return LoggerFactory.getLogger(STACK_WALKER.getCallerClass());
+ }
++ // Paper start
++ public static Logger getClassLogger() {
++ return LoggerFactory.getLogger(STACK_WALKER.getCallerClass().getSimpleName());
++ }
++ // Paper end
+ }
diff --git a/paper-server/patches/sources/com/mojang/math/OctahedralGroup.java.patch b/paper-server/patches/sources/com/mojang/math/OctahedralGroup.java.patch
new file mode 100644
index 0000000000..64c7d5d058
--- /dev/null
+++ b/paper-server/patches/sources/com/mojang/math/OctahedralGroup.java.patch
@@ -0,0 +1,30 @@
+--- a/com/mojang/math/OctahedralGroup.java
++++ b/com/mojang/math/OctahedralGroup.java
+@@ -110,6 +110,7 @@
+ this.permutation = axisTransformation;
+ this.transformation = new Matrix3f().scaling(flipX ? -1.0F : 1.0F, flipY ? -1.0F : 1.0F, flipZ ? -1.0F : 1.0F);
+ this.transformation.mul(axisTransformation.transformation());
++ this.initializeRotationDirections(); // Paper - Avoid Lazy Initialization for Enum Fields
+ }
+
+ private BooleanList packInversions() {
+@@ -138,7 +139,7 @@
+ return this.name;
+ }
+
+- public Direction rotate(Direction direction) {
++ public void initializeRotationDirections() { // Paper - Avoid Lazy Initialization for Enum Fields
+ if (this.rotatedDirections == null) {
+ this.rotatedDirections = Maps.newEnumMap(Direction.class);
+ Direction.Axis[] axiss = Direction.Axis.values();
+@@ -153,6 +154,10 @@
+ }
+ }
+
++ // Paper start - Avoid Lazy Initialization for Enum Fields
++ }
++ public Direction rotate(Direction direction) {
++ // Paper end - Avoid Lazy Initialization for Enum Fields
+ return this.rotatedDirections.get(direction);
+ }
+
diff --git a/paper-server/patches/sources/com/mojang/serialization/Dynamic.java.patch b/paper-server/patches/sources/com/mojang/serialization/Dynamic.java.patch
new file mode 100644
index 0000000000..cdd24c2f21
--- /dev/null
+++ b/paper-server/patches/sources/com/mojang/serialization/Dynamic.java.patch
@@ -0,0 +1,19 @@
+--- a/com/mojang/serialization/Dynamic.java
++++ b/com/mojang/serialization/Dynamic.java
+@@ -19,6 +19,7 @@
+
+ @SuppressWarnings("unused")
+ public class Dynamic extends DynamicLike {
++ private static final boolean DEBUG_MISSING_KEYS = Boolean.getBoolean("Paper.debugDynamicMissingKeys"); // Paper - Perf: Skip toString on values like NBT
+ private final T value;
+
+ public Dynamic(final DynamicOps ops) {
+@@ -120,7 +121,7 @@
+ return new OptionalDynamic<>(ops, ops.getMap(value).flatMap(m -> {
+ final T value = m.get(key);
+ if (value == null) {
+- return DataResult.error(() -> "key missing: " + key + " in " + this.value);
++ return DataResult.error(() -> DEBUG_MISSING_KEYS ? "key missing: " + key + " in " + this.value : "key missing: " + key); // Paper - Perf: Skip toString on values like NBT
+ }
+ return DataResult.success(new Dynamic<>(ops, value));
+ }));
diff --git a/paper-server/patches/sources/net/minecraft/ChatFormatting.java.patch b/paper-server/patches/sources/net/minecraft/ChatFormatting.java.patch
new file mode 100644
index 0000000000..eb700c93b5
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/ChatFormatting.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/ChatFormatting.java
++++ b/net/minecraft/ChatFormatting.java
+@@ -112,6 +112,18 @@
+ return name == null ? null : FORMATTING_BY_NAME.get(cleanName(name));
+ }
+
++ // Paper start - add method to get by hex value
++ @Nullable public static ChatFormatting getByHexValue(int i) {
++ for (ChatFormatting value : values()) {
++ if (value.getColor() != null && value.getColor() == i) {
++ return value;
++ }
++ }
++
++ return null;
++ }
++ // Paper end - add method to get by hex value
++
+ @Nullable
+ public static ChatFormatting getById(int colorIndex) {
+ if (colorIndex < 0) {
diff --git a/paper-server/patches/sources/net/minecraft/CrashReport.java.patch b/paper-server/patches/sources/net/minecraft/CrashReport.java.patch
new file mode 100644
index 0000000000..0b8863cd41
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/CrashReport.java.patch
@@ -0,0 +1,22 @@
+--- a/net/minecraft/CrashReport.java
++++ b/net/minecraft/CrashReport.java
+@@ -34,8 +34,10 @@
+ private final SystemReport systemReport = new SystemReport();
+
+ public CrashReport(String message, Throwable cause) {
++ io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(cause); // Paper
+ this.title = message;
+ this.exception = cause;
++ this.systemReport.setDetail("CraftBukkit Information", new org.bukkit.craftbukkit.CraftCrashReport()); // CraftBukkit
+ }
+
+ public String getTitle() {
+@@ -251,7 +253,7 @@
+ }
+
+ public static void preload() {
+- MemoryReserve.allocate();
++ // MemoryReserve.allocate(); // Paper - Disable memory reserve allocating
+ (new CrashReport("Don't panic!", new Throwable())).getFriendlyReport(ReportType.CRASH);
+ }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/CrashReportCategory.java.patch b/paper-server/patches/sources/net/minecraft/CrashReportCategory.java.patch
new file mode 100644
index 0000000000..23f5297d2e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/CrashReportCategory.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/CrashReportCategory.java
++++ b/net/minecraft/CrashReportCategory.java
+@@ -110,6 +110,7 @@
+ } else {
+ this.stackTrace = new StackTraceElement[stackTraceElements.length - 3 - ignoredCallCount];
+ System.arraycopy(stackTraceElements, 3 + ignoredCallCount, this.stackTrace, 0, this.stackTrace.length);
++ this.stackTrace = io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(this.stackTrace); // Paper
+ return this.stackTrace.length;
+ }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/Util.java.patch b/paper-server/patches/sources/net/minecraft/Util.java.patch
new file mode 100644
index 0000000000..378b82b378
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/Util.java.patch
@@ -0,0 +1,143 @@
+--- a/net/minecraft/Util.java
++++ b/net/minecraft/Util.java
+@@ -92,9 +92,26 @@
+ private static final int DEFAULT_MAX_THREADS = 255;
+ private static final int DEFAULT_SAFE_FILE_OPERATION_RETRIES = 10;
+ private static final String MAX_THREADS_SYSTEM_PROPERTY = "max.bg.threads";
+- private static final TracingExecutor BACKGROUND_EXECUTOR = makeExecutor("Main");
++ private static final TracingExecutor BACKGROUND_EXECUTOR = makeExecutor("Main", -1); // Paper - Perf: add priority
+ private static final TracingExecutor IO_POOL = makeIoExecutor("IO-Worker-", false);
++ public static final TracingExecutor DIMENSION_DATA_IO_POOL = makeExtraIoExecutor("Dimension-Data-IO-Worker-"); // Paper - Separate dimension data IO pool
+ private static final TracingExecutor DOWNLOAD_POOL = makeIoExecutor("Download-", true);
++ // Paper start - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread
++ public static final ExecutorService PROFILE_EXECUTOR = Executors.newFixedThreadPool(2, new java.util.concurrent.ThreadFactory() {
++
++ private final AtomicInteger count = new AtomicInteger();
++
++ @Override
++ public Thread newThread(Runnable run) {
++ Thread ret = new Thread(run);
++ ret.setName("Profile Lookup Executor #" + this.count.getAndIncrement());
++ ret.setUncaughtExceptionHandler((Thread thread, Throwable throwable) -> {
++ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
++ });
++ return ret;
++ }
++ });
++ // Paper end - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread
+ private static final DateTimeFormatter FILENAME_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss", Locale.ROOT);
+ public static final int LINEAR_LOOKUP_THRESHOLD = 8;
+ private static final Set ALLOWED_UNTRUSTED_LINK_PROTOCOLS = Set.of("http", "https");
+@@ -136,7 +153,7 @@
+ }
+
+ public static long getNanos() {
+- return timeSource.getAsLong();
++ return System.nanoTime(); // Paper
+ }
+
+ public static long getEpochMillis() {
+@@ -147,15 +164,16 @@
+ return FILENAME_DATE_TIME_FORMATTER.format(ZonedDateTime.now());
+ }
+
+- private static TracingExecutor makeExecutor(String name) {
++ private static TracingExecutor makeExecutor(String name, final int priorityModifier) { // Paper - Perf: add priority
+ int i = maxAllowedExecutorThreads();
+- ExecutorService executorService;
++ // Paper start - Perf: use simpler thread pool that allows 1 thread and reduce worldgen thread worker count for low core count CPUs
++ final ExecutorService executorService;
+ if (i <= 0) {
+ executorService = MoreExecutors.newDirectExecutorService();
+ } else {
+- AtomicInteger atomicInteger = new AtomicInteger(1);
+- executorService = new ForkJoinPool(i, pool -> {
+- final String string2 = "Worker-" + name + "-" + atomicInteger.getAndIncrement();
++ executorService = Executors.newFixedThreadPool(i, target -> new io.papermc.paper.util.ServerWorkerThread(target, name, priorityModifier));
++ }
++ /* final String string2 = "Worker-" + name + "-" + atomicInteger.getAndIncrement();
+ ForkJoinWorkerThread forkJoinWorkerThread = new ForkJoinWorkerThread(pool) {
+ @Override
+ protected void onStart() {
+@@ -177,13 +195,26 @@
+ forkJoinWorkerThread.setName(string2);
+ return forkJoinWorkerThread;
+ }, Util::onThreadException, true);
+- }
++ }*/
+
+ return new TracingExecutor(executorService);
+ }
+
+ public static int maxAllowedExecutorThreads() {
+- return Mth.clamp(Runtime.getRuntime().availableProcessors() - 1, 1, getMaxThreads());
++ // Paper start - Perf: use simpler thread pool that allows 1 thread and reduce worldgen thread worker count for low core count CPUs
++ final int cpus = Runtime.getRuntime().availableProcessors() / 2;
++ int maxExecutorThreads;
++ if (cpus <= 4) {
++ maxExecutorThreads = cpus <= 2 ? 1 : 2;
++ } else if (cpus <= 8) {
++ // [5, 8]
++ maxExecutorThreads = Math.max(3, cpus - 2);
++ } else {
++ maxExecutorThreads = cpus * 2 / 3;
++ }
++ maxExecutorThreads = Math.min(8, maxExecutorThreads);
++ return Integer.getInteger("Paper.WorkerThreadCount", maxExecutorThreads);
++ // Paper end - Perf: use simpler thread pool that allows 1 thread and reduce worldgen thread worker count for low core count CPUs
+ }
+
+ private static int getMaxThreads() {
+@@ -229,10 +260,25 @@
+ TracyClient.setThreadName(string2, namePrefix.hashCode());
+ thread.setName(string2);
+ thread.setDaemon(daemon);
++ thread.setUncaughtExceptionHandler(Util::onThreadException);
++ return thread;
++ }));
++ }
++
++ // Paper start - Separate dimension data IO pool
++ private static TracingExecutor makeExtraIoExecutor(String namePrefix) {
++ AtomicInteger atomicInteger = new AtomicInteger(1);
++ return new TracingExecutor(Executors.newFixedThreadPool(4, runnable -> {
++ Thread thread = new Thread(runnable);
++ String string2 = namePrefix + atomicInteger.getAndIncrement();
++ TracyClient.setThreadName(string2, namePrefix.hashCode());
++ thread.setName(string2);
++ thread.setDaemon(false);
+ thread.setUncaughtExceptionHandler(Util::onThreadException);
+ return thread;
+ }));
+ }
++ // Paper end - Separate dimension data IO pool
+
+ public static void throwAsRuntime(Throwable t) {
+ throw t instanceof RuntimeException ? (RuntimeException)t : new RuntimeException(t);
+@@ -537,7 +583,7 @@
+ public static , V> EnumMap makeEnumMap(Class enumClass, Function mapper) {
+ EnumMap enumMap = new EnumMap<>(enumClass);
+
+- for (K enum_ : (Enum[])enumClass.getEnumConstants()) {
++ for (K enum_ : enumClass.getEnumConstants()) { // Paper - decompile error
+ enumMap.put(enum_, mapper.apply(enum_));
+ }
+
+@@ -1057,16 +1103,7 @@
+ }
+
+ public void openUri(URI uri) {
+- try {
+- Process process = AccessController.doPrivileged(
+- (PrivilegedExceptionAction)(() -> Runtime.getRuntime().exec(this.getOpenUriArguments(uri)))
+- );
+- process.getInputStream().close();
+- process.getErrorStream().close();
+- process.getOutputStream().close();
+- } catch (IOException | PrivilegedActionException var3) {
+- Util.LOGGER.error("Couldn't open location '{}'", uri, var3);
+- }
++ throw new IllegalStateException("This method is not useful on dedicated servers."); // Paper - Fix warnings on build by removing client-only code
+ }
+
+ public void openFile(File file) {
diff --git a/paper-server/patches/sources/net/minecraft/advancements/AdvancementHolder.java.patch b/paper-server/patches/sources/net/minecraft/advancements/AdvancementHolder.java.patch
new file mode 100644
index 0000000000..d41639df88
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/advancements/AdvancementHolder.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/advancements/AdvancementHolder.java
++++ b/net/minecraft/advancements/AdvancementHolder.java
+@@ -5,6 +5,10 @@
+ import net.minecraft.network.codec.ByteBufCodecs;
+ import net.minecraft.network.codec.StreamCodec;
+ import net.minecraft.resources.ResourceLocation;
++// CraftBukkit start
++import org.bukkit.craftbukkit.advancement.CraftAdvancement;
++import org.bukkit.craftbukkit.util.CraftNamespacedKey;
++// CraftBukkit end
+
+ public record AdvancementHolder(ResourceLocation id, Advancement value) {
+
+@@ -38,4 +42,10 @@
+ public String toString() {
+ return this.id.toString();
+ }
++
++ // CraftBukkit start
++ public final org.bukkit.advancement.Advancement toBukkit() {
++ return new CraftAdvancement(this);
++ }
++ // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/advancements/AdvancementTree.java.patch b/paper-server/patches/sources/net/minecraft/advancements/AdvancementTree.java.patch
new file mode 100644
index 0000000000..4bec8c46eb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/advancements/AdvancementTree.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/advancements/AdvancementTree.java
++++ b/net/minecraft/advancements/AdvancementTree.java
+@@ -35,7 +35,7 @@
+ this.remove(advancementnode1);
+ }
+
+- AdvancementTree.LOGGER.info("Forgot about advancement {}", advancement.holder());
++ AdvancementTree.LOGGER.debug("Forgot about advancement {}", advancement.holder()); // Paper - Improve logging and errors
+ this.nodes.remove(advancement.holder().id());
+ if (advancement.parent() == null) {
+ this.roots.remove(advancement);
+@@ -77,7 +77,7 @@
+ }
+ }
+
+- AdvancementTree.LOGGER.info("Loaded {} advancements", this.nodes.size());
++ // AdvancementTree.LOGGER.info("Loaded {} advancements", this.nodes.size()); // CraftBukkit - moved to AdvancementDataWorld#reload // Paper - Improve logging and errors; you say it was moved... but it wasn't :) it should be moved however, since this is called when the API creates an advancement
+ }
+
+ private boolean tryInsert(AdvancementHolder advancement) {
diff --git a/paper-server/patches/sources/net/minecraft/advancements/DisplayInfo.java.patch b/paper-server/patches/sources/net/minecraft/advancements/DisplayInfo.java.patch
new file mode 100644
index 0000000000..a0fdd33f43
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/advancements/DisplayInfo.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/advancements/DisplayInfo.java
++++ b/net/minecraft/advancements/DisplayInfo.java
+@@ -37,6 +37,7 @@
+ private final boolean hidden;
+ private float x;
+ private float y;
++ public final io.papermc.paper.advancement.AdvancementDisplay paper = new io.papermc.paper.advancement.PaperAdvancementDisplay(this); // Paper - Add more advancement API
+
+ public DisplayInfo(
+ ItemStack icon,
diff --git a/paper-server/patches/sources/net/minecraft/advancements/critereon/LocationPredicate.java.patch b/paper-server/patches/sources/net/minecraft/advancements/critereon/LocationPredicate.java.patch
new file mode 100644
index 0000000000..7f7873fc42
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/advancements/critereon/LocationPredicate.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/advancements/critereon/LocationPredicate.java
++++ b/net/minecraft/advancements/critereon/LocationPredicate.java
+@@ -44,7 +44,7 @@
+ public boolean matches(ServerLevel world, double x, double y, double z) {
+ if (this.position.isPresent() && !this.position.get().matches(x, y, z)) {
+ return false;
+- } else if (this.dimension.isPresent() && this.dimension.get() != world.dimension()) {
++ } else if (this.dimension.isPresent() && this.dimension.get() != (io.papermc.paper.configuration.GlobalConfiguration.get().misc.strictAdvancementDimensionCheck ? world.dimension() : org.bukkit.craftbukkit.util.CraftDimensionUtil.getMainDimensionKey(world))) { // Paper - Add option for strict advancement dimension checks
+ return false;
+ } else {
+ BlockPos blockPos = BlockPos.containing(x, y, z);
diff --git a/paper-server/patches/sources/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java.patch b/paper-server/patches/sources/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java.patch
new file mode 100644
index 0000000000..d080616002
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java.patch
@@ -0,0 +1,52 @@
+--- a/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java
++++ b/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java
+@@ -15,41 +15,41 @@
+ import net.minecraft.world.level.storage.loot.LootContext;
+
+ public abstract class SimpleCriterionTrigger implements CriterionTrigger {
+- private final Map>> players = Maps.newIdentityHashMap();
++ // private final Map>> players = Maps.newIdentityHashMap(); // Paper - fix AdvancementDataPlayer leak; moved into AdvancementDataPlayer to fix memory leak
+
+ @Override
+ public final void addPlayerListener(PlayerAdvancements manager, CriterionTrigger.Listener conditions) {
+- this.players.computeIfAbsent(manager, managerx -> Sets.newHashSet()).add(conditions);
++ manager.criterionData.computeIfAbsent(this, managerx -> Sets.newHashSet()).add(conditions); // Paper - fix AdvancementDataPlayer leak
+ }
+
+ @Override
+ public final void removePlayerListener(PlayerAdvancements manager, CriterionTrigger.Listener conditions) {
+- Set> set = this.players.get(manager);
++ Set> set = (Set) manager.criterionData.get(this); // Paper - fix AdvancementDataPlayer leak
+ if (set != null) {
+ set.remove(conditions);
+ if (set.isEmpty()) {
+- this.players.remove(manager);
++ manager.criterionData.remove(this); // Paper - fix AdvancementDataPlayer leak
+ }
+ }
+ }
+
+ @Override
+ public final void removePlayerListeners(PlayerAdvancements tracker) {
+- this.players.remove(tracker);
++ tracker.criterionData.remove(this); // Paper - fix AdvancementDataPlayer leak
+ }
+
+ protected void trigger(ServerPlayer player, Predicate predicate) {
+ PlayerAdvancements playerAdvancements = player.getAdvancements();
+- Set> set = this.players.get(playerAdvancements);
++ Set> set = (Set) playerAdvancements.criterionData.get(this); // Paper - fix AdvancementDataPlayer leak
+ if (set != null && !set.isEmpty()) {
+- LootContext lootContext = EntityPredicate.createContext(player, player);
++ LootContext lootContext = null; // EntityPredicate.createContext(player, player); // Paper - Perf: lazily create LootContext for criterions
+ List> list = null;
+
+ for (CriterionTrigger.Listener listener : set) {
+ T simpleInstance = listener.trigger();
+ if (predicate.test(simpleInstance)) {
+ Optional optional = simpleInstance.player();
+- if (optional.isEmpty() || optional.get().matches(lootContext)) {
++ if (optional.isEmpty() || optional.get().matches(lootContext = (lootContext == null ? EntityPredicate.createContext(player, player) : lootContext))) { // Paper - Perf: lazily create LootContext for criterions
+ if (list == null) {
+ list = Lists.newArrayList();
+ }
diff --git a/paper-server/patches/sources/net/minecraft/commands/CommandSource.java.patch b/paper-server/patches/sources/net/minecraft/commands/CommandSource.java.patch
new file mode 100644
index 0000000000..4d74948c36
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/commands/CommandSource.java.patch
@@ -0,0 +1,23 @@
+--- a/net/minecraft/commands/CommandSource.java
++++ b/net/minecraft/commands/CommandSource.java
+@@ -22,6 +22,13 @@
+ public boolean shouldInformAdmins() {
+ return false;
+ }
++
++ // CraftBukkit start
++ @Override
++ public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper) {
++ return io.papermc.paper.brigadier.NullCommandSender.INSTANCE; // Paper - expose a no-op CommandSender
++ }
++ // CraftBukkit end
+ };
+
+ void sendSystemMessage(Component message);
+@@ -35,4 +42,6 @@
+ default boolean alwaysAccepts() {
+ return false;
+ }
++
++ org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper); // CraftBukkit
+ }
diff --git a/paper-server/patches/sources/net/minecraft/commands/CommandSourceStack.java.patch b/paper-server/patches/sources/net/minecraft/commands/CommandSourceStack.java.patch
new file mode 100644
index 0000000000..94ee38720d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/commands/CommandSourceStack.java.patch
@@ -0,0 +1,114 @@
+--- a/net/minecraft/commands/CommandSourceStack.java
++++ b/net/minecraft/commands/CommandSourceStack.java
+@@ -45,9 +45,9 @@
+ import net.minecraft.world.level.dimension.DimensionType;
+ import net.minecraft.world.phys.Vec2;
+ import net.minecraft.world.phys.Vec3;
++import com.mojang.brigadier.tree.CommandNode; // CraftBukkit
+
+-public class CommandSourceStack implements ExecutionCommandSource, SharedSuggestionProvider {
+-
++public class CommandSourceStack implements ExecutionCommandSource, SharedSuggestionProvider, io.papermc.paper.command.brigadier.PaperCommandSourceStack { // Paper - Brigadier API
+ public static final SimpleCommandExceptionType ERROR_NOT_PLAYER = new SimpleCommandExceptionType(Component.translatable("permissions.requires.player"));
+ public static final SimpleCommandExceptionType ERROR_NOT_ENTITY = new SimpleCommandExceptionType(Component.translatable("permissions.requires.entity"));
+ public final CommandSource source;
+@@ -65,6 +65,8 @@
+ private final Vec2 rotation;
+ private final CommandSigningContext signingContext;
+ private final TaskChainer chatMessageChainer;
++ public java.util.Map currentCommand = new java.util.concurrent.ConcurrentHashMap<>(); // CraftBukkit // Paper - Thread Safe Vanilla Command permission checking
++ public boolean bypassSelectorPermissions = false; // Paper - add bypass for selector permissions
+
+ public CommandSourceStack(CommandSource output, Vec3 pos, Vec2 rot, ServerLevel world, int level, String name, Component displayName, MinecraftServer server, @Nullable Entity entity) {
+ this(output, pos, rot, world, level, name, displayName, server, entity, false, CommandResultCallback.EMPTY, EntityAnchorArgument.Anchor.FEET, CommandSigningContext.ANONYMOUS, TaskChainer.immediate(server));
+@@ -171,8 +173,43 @@
+
+ @Override
+ public boolean hasPermission(int level) {
++ // CraftBukkit start
++ // Paper start - Thread Safe Vanilla Command permission checking
++ CommandNode currentCommand = this.currentCommand.get(Thread.currentThread());
++ if (currentCommand != null) {
++ return this.hasPermission(level, org.bukkit.craftbukkit.command.VanillaCommandWrapper.getPermission(currentCommand));
++ // Paper end - Thread Safe Vanilla Command permission checking
++ }
++ // CraftBukkit end
++
+ return this.permissionLevel >= level;
++ }
++
++ // Paper start - Fix permission levels for command blocks
++ private boolean forceRespectPermissionLevel() {
++ return this.source == CommandSource.NULL || (this.source instanceof final net.minecraft.world.level.BaseCommandBlock commandBlock && commandBlock.getLevel().paperConfig().commandBlocks.forceFollowPermLevel);
++ }
++ // Paper end - Fix permission levels for command blocks
++
++ // CraftBukkit start
++ public boolean hasPermission(int i, String bukkitPermission) {
++ // Paper start - Fix permission levels for command blocks
++ final java.util.function.BooleanSupplier hasBukkitPerm = () -> this.source == CommandSource.NULL /*treat NULL as having all bukkit perms*/ || this.getBukkitSender().hasPermission(bukkitPermission); // lazily check bukkit perms to the benefit of custom permission setups
++ // if the server is null, we must check the vanilla perm level system
++ // if ignoreVanillaPermissions is true, we can skip vanilla perms and just run the bukkit perm check
++ //noinspection ConstantValue
++ if (this.getServer() == null || !this.getServer().server.ignoreVanillaPermissions) { // server & level are null for command function loading
++ final boolean hasPermLevel = this.permissionLevel >= i;
++ if (this.forceRespectPermissionLevel()) { // NULL CommandSource and command blocks (if setting is enabled) should always pass the vanilla perm check
++ return hasPermLevel && hasBukkitPerm.getAsBoolean();
++ } else { // otherwise check vanilla perm first then bukkit perm, matching upstream behavior
++ return hasPermLevel || hasBukkitPerm.getAsBoolean();
++ }
++ }
++ return hasBukkitPerm.getAsBoolean();
++ // Paper end - Fix permission levels for command blocks
+ }
++ // CraftBukkit end
+
+ public Vec3 getPosition() {
+ return this.worldPosition;
+@@ -302,21 +339,26 @@
+ while (iterator.hasNext()) {
+ ServerPlayer entityplayer = (ServerPlayer) iterator.next();
+
+- if (entityplayer.commandSource() != this.source && this.server.getPlayerList().isOp(entityplayer.getGameProfile())) {
++ if (entityplayer.commandSource() != this.source && entityplayer.getBukkitEntity().hasPermission("minecraft.admin.command_feedback")) { // CraftBukkit
+ entityplayer.sendSystemMessage(ichatmutablecomponent);
+ }
+ }
+ }
+
+- if (this.source != this.server && this.server.getGameRules().getBoolean(GameRules.RULE_LOGADMINCOMMANDS)) {
++ if (this.source != this.server && this.server.getGameRules().getBoolean(GameRules.RULE_LOGADMINCOMMANDS) && !org.spigotmc.SpigotConfig.silentCommandBlocks) { // Spigot
+ this.server.sendSystemMessage(ichatmutablecomponent);
+ }
+
+ }
+
+ public void sendFailure(Component message) {
++ // Paper start - Add UnknownCommandEvent
++ this.sendFailure(message, true);
++ }
++ public void sendFailure(Component message, boolean withStyle) {
++ // Paper end - Add UnknownCommandEvent
+ if (this.source.acceptsFailure() && !this.silent) {
+- this.source.sendSystemMessage(Component.empty().append(message).withStyle(ChatFormatting.RED));
++ this.source.sendSystemMessage(withStyle ? Component.empty().append(message).withStyle(ChatFormatting.RED) : message); // Paper - Add UnknownCommandEvent
+ }
+
+ }
+@@ -400,4 +442,16 @@
+ public boolean isSilent() {
+ return this.silent;
+ }
++
++ // Paper start
++ @Override
++ public CommandSourceStack getHandle() {
++ return this;
++ }
++ // Paper end
++ // CraftBukkit start
++ public org.bukkit.command.CommandSender getBukkitSender() {
++ return this.source.getBukkitSender(this);
++ }
++ // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch b/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch
new file mode 100644
index 0000000000..1fc19d5217
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch
@@ -0,0 +1,372 @@
+--- a/net/minecraft/commands/Commands.java
++++ b/net/minecraft/commands/Commands.java
+@@ -138,6 +138,14 @@
+ import net.minecraft.world.flag.FeatureFlags;
+ import net.minecraft.world.level.GameRules;
+ import org.slf4j.Logger;
++
++// CraftBukkit start
++import com.google.common.base.Joiner;
++import java.util.Collection;
++import java.util.LinkedHashSet;
++import org.bukkit.event.player.PlayerCommandSendEvent;
++import org.bukkit.event.server.ServerCommandEvent;
++// CraftBukkit end
+
+ public class Commands {
+
+@@ -151,6 +159,7 @@
+ private final com.mojang.brigadier.CommandDispatcher dispatcher = new com.mojang.brigadier.CommandDispatcher();
+
+ public Commands(Commands.CommandSelection environment, CommandBuildContext commandRegistryAccess) {
++ // Paper
+ AdvancementCommands.register(this.dispatcher);
+ AttributeCommand.register(this.dispatcher, commandRegistryAccess);
+ ExecuteCommand.register(this.dispatcher, commandRegistryAccess);
+@@ -250,8 +259,33 @@
+
+ if (environment.includeIntegrated) {
+ PublishCommand.register(this.dispatcher);
++ }
++
++ // Paper start - Vanilla command permission fixes
++ for (final CommandNode node : this.dispatcher.getRoot().getChildren()) {
++ if (node.getRequirement() == com.mojang.brigadier.builder.ArgumentBuilder.defaultRequirement()) {
++ node.requirement = stack -> stack.source == CommandSource.NULL || stack.getBukkitSender().hasPermission(org.bukkit.craftbukkit.command.VanillaCommandWrapper.getPermission(node));
++ }
+ }
++ // Paper end - Vanilla command permission fixes
++ // Paper start - Brigadier Command API
++ // Create legacy minecraft namespace commands
++ for (final CommandNode node : new java.util.ArrayList<>(this.dispatcher.getRoot().getChildren())) {
++ // The brigadier dispatcher is not able to resolve nested redirects.
++ // E.g. registering the alias minecraft:tp cannot redirect to tp, as tp itself redirects to teleport.
++ // Instead, target the first none redirecting node.
++ CommandNode flattenedAliasTarget = node;
++ while (flattenedAliasTarget.getRedirect() != null) flattenedAliasTarget = flattenedAliasTarget.getRedirect();
+
++ this.dispatcher.register(
++ com.mojang.brigadier.builder.LiteralArgumentBuilder.literal("minecraft:" + node.getName())
++ .executes(flattenedAliasTarget.getCommand())
++ .requires(flattenedAliasTarget.getRequirement())
++ .redirect(flattenedAliasTarget)
++ );
++ }
++ // Paper end - Brigadier Command API
++ // Paper - remove public constructor, no longer needed
+ this.dispatcher.setConsumer(ExecutionCommandSource.resultConsumer());
+ }
+
+@@ -262,30 +296,72 @@
+ return new ParseResults(commandcontextbuilder1, parseResults.getReader(), parseResults.getExceptions());
+ }
+
++ // CraftBukkit start
++ public void dispatchServerCommand(CommandSourceStack sender, String command) {
++ Joiner joiner = Joiner.on(" ");
++ if (command.startsWith("/")) {
++ command = command.substring(1);
++ }
++
++ ServerCommandEvent event = new ServerCommandEvent(sender.getBukkitSender(), command);
++ org.bukkit.Bukkit.getPluginManager().callEvent(event);
++ if (event.isCancelled()) {
++ return;
++ }
++ command = event.getCommand();
++
++ String[] args = command.split(" ");
++ if (args.length == 0) return; // Paper - empty commands shall not be dispatched
++
++ // Paper - Fix permission levels for command blocks
++
++ // Handle vanilla commands; // Paper - handled in CommandNode/CommandDispatcher
++
++ String newCommand = joiner.join(args);
++ this.performPrefixedCommand(sender, newCommand, newCommand);
++ }
++ // CraftBukkit end
++
+ public void performPrefixedCommand(CommandSourceStack source, String command) {
+- command = command.startsWith("/") ? command.substring(1) : command;
+- this.performCommand(this.dispatcher.parse(command, source), command);
++ // CraftBukkit start
++ this.performPrefixedCommand(source, command, command);
++ }
++
++ public void performPrefixedCommand(CommandSourceStack commandlistenerwrapper, String s, String label) {
++ s = s.startsWith("/") ? s.substring(1) : s;
++ this.performCommand(this.dispatcher.parse(s, commandlistenerwrapper), s, label);
++ // CraftBukkit end
+ }
+
+ public void performCommand(ParseResults parseResults, String command) {
+- CommandSourceStack commandlistenerwrapper = (CommandSourceStack) parseResults.getContext().getSource();
++ this.performCommand(parseResults, command, command);
++ }
+
++ public void performCommand(ParseResults parseresults, String s, String label) { // CraftBukkit
++ // Paper start
++ this.performCommand(parseresults, s, label, false);
++ }
++ public void performCommand(ParseResults parseresults, String s, String label, boolean throwCommandError) {
++ // Paper end
++ CommandSourceStack commandlistenerwrapper = (CommandSourceStack) parseresults.getContext().getSource();
++
+ Profiler.get().push(() -> {
+- return "/" + command;
++ return "/" + s;
+ });
+- ContextChain contextchain = Commands.finishParsing(parseResults, command, commandlistenerwrapper);
++ ContextChain contextchain = this.finishParsing(parseresults, s, commandlistenerwrapper, label); // CraftBukkit // Paper - Add UnknownCommandEvent
+
+ try {
+ if (contextchain != null) {
+ Commands.executeCommandInContext(commandlistenerwrapper, (executioncontext) -> {
+- ExecutionContext.queueInitialCommandExecution(executioncontext, command, contextchain, commandlistenerwrapper, CommandResultCallback.EMPTY);
++ ExecutionContext.queueInitialCommandExecution(executioncontext, s, contextchain, commandlistenerwrapper, CommandResultCallback.EMPTY);
+ });
+ }
+ } catch (Exception exception) {
++ if (throwCommandError) throw exception;
+ MutableComponent ichatmutablecomponent = Component.literal(exception.getMessage() == null ? exception.getClass().getName() : exception.getMessage());
+
+- if (Commands.LOGGER.isDebugEnabled()) {
+- Commands.LOGGER.error("Command exception: /{}", command, exception);
++ Commands.LOGGER.error("Command exception: /{}", s, exception); // Paper - always show execution exception in console log
++ if (commandlistenerwrapper.getServer().isDebugging() || Commands.LOGGER.isDebugEnabled()) { // Paper - Debugging
+ StackTraceElement[] astacktraceelement = exception.getStackTrace();
+
+ for (int i = 0; i < Math.min(astacktraceelement.length, 3); ++i) {
+@@ -298,7 +374,7 @@
+ }));
+ if (SharedConstants.IS_RUNNING_IN_IDE) {
+ commandlistenerwrapper.sendFailure(Component.literal(Util.describeError(exception)));
+- Commands.LOGGER.error("'/{}' threw an exception", command, exception);
++ Commands.LOGGER.error("'/{}' threw an exception", s, exception);
+ }
+ } finally {
+ Profiler.get().pop();
+@@ -307,18 +383,22 @@
+ }
+
+ @Nullable
+- private static ContextChain finishParsing(ParseResults parseResults, String command, CommandSourceStack source) {
++ private ContextChain finishParsing(ParseResults parseresults, String s, CommandSourceStack commandlistenerwrapper, String label) { // CraftBukkit // Paper - Add UnknownCommandEvent
+ try {
+- Commands.validateParseResults(parseResults);
+- return (ContextChain) ContextChain.tryFlatten(parseResults.getContext().build(command)).orElseThrow(() -> {
+- return CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand().createWithContext(parseResults.getReader());
++ Commands.validateParseResults(parseresults);
++ return (ContextChain) ContextChain.tryFlatten(parseresults.getContext().build(s)).orElseThrow(() -> {
++ return CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand().createWithContext(parseresults.getReader());
+ });
+ } catch (CommandSyntaxException commandsyntaxexception) {
+- source.sendFailure(ComponentUtils.fromMessage(commandsyntaxexception.getRawMessage()));
++ // Paper start - Add UnknownCommandEvent
++ final net.kyori.adventure.text.TextComponent.Builder builder = net.kyori.adventure.text.Component.text();
++ // commandlistenerwrapper.sendFailure(ComponentUtils.fromMessage(commandsyntaxexception.getRawMessage()));
++ builder.color(net.kyori.adventure.text.format.NamedTextColor.RED).append(io.papermc.paper.brigadier.PaperBrigadier.componentFromMessage(commandsyntaxexception.getRawMessage()));
++ // Paper end - Add UnknownCommandEvent
+ if (commandsyntaxexception.getInput() != null && commandsyntaxexception.getCursor() >= 0) {
+ int i = Math.min(commandsyntaxexception.getInput().length(), commandsyntaxexception.getCursor());
+ MutableComponent ichatmutablecomponent = Component.empty().withStyle(ChatFormatting.GRAY).withStyle((chatmodifier) -> {
+- return chatmodifier.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/" + command));
++ return chatmodifier.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/" + label)); // CraftBukkit // Paper
+ });
+
+ if (i > 10) {
+@@ -333,8 +413,18 @@
+ }
+
+ ichatmutablecomponent.append((Component) Component.translatable("command.context.here").withStyle(ChatFormatting.RED, ChatFormatting.ITALIC));
+- source.sendFailure(ichatmutablecomponent);
++ // Paper start - Add UnknownCommandEvent
++ // commandlistenerwrapper.sendFailure(ichatmutablecomponent);
++ builder
++ .append(net.kyori.adventure.text.Component.newline())
++ .append(io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent));
+ }
++ org.bukkit.event.command.UnknownCommandEvent event = new org.bukkit.event.command.UnknownCommandEvent(commandlistenerwrapper.getBukkitSender(), s, org.spigotmc.SpigotConfig.unknownCommandMessage.isEmpty() ? null : builder.build());
++ org.bukkit.Bukkit.getServer().getPluginManager().callEvent(event);
++ if (event.message() != null) {
++ commandlistenerwrapper.sendFailure(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.message()), false);
++ // Paper end - Add UnknownCommandEvent
++ }
+
+ return null;
+ }
+@@ -368,7 +458,7 @@
+
+ executioncontext1.close();
+ } finally {
+- Commands.CURRENT_EXECUTION_CONTEXT.set((Object) null);
++ Commands.CURRENT_EXECUTION_CONTEXT.set(null); // CraftBukkit - decompile error
+ }
+ } else {
+ callback.accept(executioncontext);
+@@ -377,30 +467,133 @@
+ }
+
+ public void sendCommands(ServerPlayer player) {
+- Map, CommandNode> map = Maps.newHashMap();
++ // Paper start - Send empty commands if tab completion is disabled
++ if (org.spigotmc.SpigotConfig.tabComplete < 0) {
++ player.connection.send(new ClientboundCommandsPacket(new RootCommandNode<>()));
++ return;
++ }
++ // Paper end - Send empty commands if tab completion is disabled
++ // CraftBukkit start
++ // Register Vanilla commands into builtRoot as before
++ // Paper start - Perf: Async command map building
++ // Copy root children to avoid concurrent modification during building
++ final Collection> commandNodes = new java.util.ArrayList<>(this.dispatcher.getRoot().getChildren());
++ COMMAND_SENDING_POOL.execute(() -> this.sendAsync(player, commandNodes));
++ }
++
++ // Fixed pool, but with discard policy
++ public static final java.util.concurrent.ExecutorService COMMAND_SENDING_POOL = new java.util.concurrent.ThreadPoolExecutor(
++ 2, 2, 0, java.util.concurrent.TimeUnit.MILLISECONDS,
++ new java.util.concurrent.LinkedBlockingQueue<>(),
++ new com.google.common.util.concurrent.ThreadFactoryBuilder()
++ .setNameFormat("Paper Async Command Builder Thread Pool - %1$d")
++ .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER))
++ .build(),
++ new java.util.concurrent.ThreadPoolExecutor.DiscardPolicy()
++ );
++
++ private void sendAsync(ServerPlayer player, Collection> dispatcherRootChildren) {
++ // Paper end - Perf: Async command map building
++ Map, CommandNode> map = Maps.newIdentityHashMap(); // Use identity to prevent aliasing issues
++ // Paper - brigadier API removes the need to fill the map twice
+ RootCommandNode rootcommandnode = new RootCommandNode();
+
+ map.put(this.dispatcher.getRoot(), rootcommandnode);
+- this.fillUsableCommands(this.dispatcher.getRoot(), rootcommandnode, player.createCommandSourceStack(), map);
++ this.fillUsableCommands(dispatcherRootChildren, rootcommandnode, player.createCommandSourceStack(), map); // Paper - Perf: Async command map building; pass copy of children
++
++ Collection bukkit = new LinkedHashSet<>();
++ for (CommandNode node : rootcommandnode.getChildren()) {
++ bukkit.add(node.getName());
++ }
++ // Paper start - Perf: Async command map building
++ new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent(player.getBukkitEntity(), (RootCommandNode) rootcommandnode, false).callEvent(); // Paper - Brigadier API
++ net.minecraft.server.MinecraftServer.getServer().execute(() -> {
++ runSync(player, bukkit, rootcommandnode);
++ });
++ }
++
++ private void runSync(ServerPlayer player, Collection bukkit, RootCommandNode rootcommandnode) {
++ // Paper end - Perf: Async command map building
++ new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent(player.getBukkitEntity(), (RootCommandNode) rootcommandnode, true).callEvent(); // Paper - Brigadier API
++ PlayerCommandSendEvent event = new PlayerCommandSendEvent(player.getBukkitEntity(), new LinkedHashSet<>(bukkit));
++ event.getPlayer().getServer().getPluginManager().callEvent(event);
++
++ // Remove labels that were removed during the event
++ for (String orig : bukkit) {
++ if (!event.getCommands().contains(orig)) {
++ rootcommandnode.removeCommand(orig);
++ }
++ }
++ // CraftBukkit end
+ player.connection.send(new ClientboundCommandsPacket(rootcommandnode));
+ }
+
+- private void fillUsableCommands(CommandNode tree, CommandNode result, CommandSourceStack source, Map, CommandNode> resultNodes) {
+- Iterator iterator = tree.getChildren().iterator();
++ // Paper start - Perf: Async command map building; pass copy of children
++ private void fillUsableCommands(Collection> children, CommandNode result, CommandSourceStack source, Map, CommandNode> resultNodes) {
++ resultNodes.keySet().removeIf((node) -> !org.spigotmc.SpigotConfig.sendNamespaced && node.getName().contains( ":" )); // Paper - Remove namedspaced from result nodes to prevent redirect trimming ~ see comment below
++ Iterator iterator = children.iterator();
++ // Paper end - Perf: Async command map building
+
+ while (iterator.hasNext()) {
+ CommandNode commandnode2 = (CommandNode) iterator.next();
++ // Paper start - Brigadier API
++ if (commandnode2.clientNode != null) {
++ commandnode2 = commandnode2.clientNode;
++ }
++ // Paper end - Brigadier API
++ if ( !org.spigotmc.SpigotConfig.sendNamespaced && commandnode2.getName().contains( ":" ) ) continue; // Spigot
+
+ if (commandnode2.canUse(source)) {
+- ArgumentBuilder argumentbuilder = commandnode2.createBuilder();
++ ArgumentBuilder argumentbuilder = commandnode2.createBuilder(); // CraftBukkit - decompile error
++ // Paper start
++ /*
++ Because of how commands can be yeeted right left and center due to bad bukkit practices
++ we need to be able to ensure that ALL commands are registered (even redirects).
+
++ What this will do is IF the redirect seems to be "dead" it will create a builder and essentially populate (flatten)
++ all the children from the dead redirect to the node.
++
++ So, if minecraft:msg redirects to msg but the original msg node has been overriden minecraft:msg will now act as msg and will explicilty inherit its children.
++
++ The only way to fix this is to either:
++ - Send EVERYTHING flattened, don't use redirects
++ - Don't allow command nodes to be deleted
++ - Do this :)
++ */
++
++ // Is there an invalid command redirect?
++ if (argumentbuilder.getRedirect() != null && (CommandNode) resultNodes.get(argumentbuilder.getRedirect()) == null) {
++ // Create the argument builder with the same values as the specified node, but with a different literal and populated children
++
++ CommandNode redirect = argumentbuilder.getRedirect();
++ // Diff copied from LiteralCommand#createBuilder
++ final com.mojang.brigadier.builder.LiteralArgumentBuilder builder = com.mojang.brigadier.builder.LiteralArgumentBuilder.literal(commandnode2.getName());
++ builder.requires(redirect.getRequirement());
++ // builder.forward(redirect.getRedirect(), redirect.getRedirectModifier(), redirect.isFork()); We don't want to migrate the forward, since it's invalid.
++ if (redirect.getCommand() != null) {
++ builder.executes(redirect.getCommand());
++ }
++ // Diff copied from LiteralCommand#createBuilder
++ for (CommandNode child : redirect.getChildren()) {
++ builder.then(child);
++ }
++
++ argumentbuilder = builder;
++ }
++ // Paper end
++
+ argumentbuilder.requires((icompletionprovider) -> {
+ return true;
+ });
+ if (argumentbuilder.getCommand() != null) {
+- argumentbuilder.executes((commandcontext) -> {
+- return 0;
++ // Paper start - fix suggestions due to falsely equal nodes
++ argumentbuilder.executes(new com.mojang.brigadier.Command() {
++ @Override
++ public int run(com.mojang.brigadier.context.CommandContext commandContext) throws CommandSyntaxException {
++ return 0;
++ }
+ });
++ // Paper end
+ }
+
+ if (argumentbuilder instanceof RequiredArgumentBuilder) {
+@@ -415,12 +608,12 @@
+ argumentbuilder.redirect((CommandNode) resultNodes.get(argumentbuilder.getRedirect()));
+ }
+
+- CommandNode commandnode3 = argumentbuilder.build();
++ CommandNode commandnode3 = argumentbuilder.build(); // CraftBukkit - decompile error
+
+ resultNodes.put(commandnode2, commandnode3);
+ result.addChild(commandnode3);
+ if (!commandnode2.getChildren().isEmpty()) {
+- this.fillUsableCommands(commandnode2, commandnode3, source, resultNodes);
++ this.fillUsableCommands(commandnode2.getChildren(), commandnode3, source, resultNodes); // Paper - Perf: Async command map building; pass children directly
+ }
+ }
+ }
+@@ -481,7 +674,7 @@
+ }
+
+ private HolderLookup.RegistryLookup.Delegate createLookup(final HolderLookup.RegistryLookup original) {
+- return new HolderLookup.RegistryLookup.Delegate(this) {
++ return new HolderLookup.RegistryLookup.Delegate() { // CraftBukkit - decompile error
+ @Override
+ public HolderLookup.RegistryLookup parent() {
+ return original;
diff --git a/paper-server/patches/sources/net/minecraft/commands/arguments/EntityArgument.java.patch b/paper-server/patches/sources/net/minecraft/commands/arguments/EntityArgument.java.patch
new file mode 100644
index 0000000000..9c45cfda8f
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/commands/arguments/EntityArgument.java.patch
@@ -0,0 +1,52 @@
+--- a/net/minecraft/commands/arguments/EntityArgument.java
++++ b/net/minecraft/commands/arguments/EntityArgument.java
+@@ -102,21 +102,27 @@
+ }
+
+ private EntitySelector parse(StringReader reader, boolean allowAtSelectors) throws CommandSyntaxException {
++ // CraftBukkit start
++ return this.parse(reader, allowAtSelectors, false);
++ }
++
++ public EntitySelector parse(StringReader stringreader, boolean flag, boolean overridePermissions) throws CommandSyntaxException {
++ // CraftBukkit end
+ boolean flag1 = false;
+- EntitySelectorParser argumentparserselector = new EntitySelectorParser(reader, allowAtSelectors);
+- EntitySelector entityselector = argumentparserselector.parse();
++ EntitySelectorParser argumentparserselector = new EntitySelectorParser(stringreader, flag);
++ EntitySelector entityselector = argumentparserselector.parse(overridePermissions); // CraftBukkit
+
+ if (entityselector.getMaxResults() > 1 && this.single) {
+ if (this.playersOnly) {
+- reader.setCursor(0);
+- throw EntityArgument.ERROR_NOT_SINGLE_PLAYER.createWithContext(reader);
++ stringreader.setCursor(0);
++ throw EntityArgument.ERROR_NOT_SINGLE_PLAYER.createWithContext(stringreader);
+ } else {
+- reader.setCursor(0);
+- throw EntityArgument.ERROR_NOT_SINGLE_ENTITY.createWithContext(reader);
++ stringreader.setCursor(0);
++ throw EntityArgument.ERROR_NOT_SINGLE_ENTITY.createWithContext(stringreader);
+ }
+ } else if (entityselector.includesEntities() && this.playersOnly && !entityselector.isSelfSelector()) {
+- reader.setCursor(0);
+- throw EntityArgument.ERROR_ONLY_PLAYERS_ALLOWED.createWithContext(reader);
++ stringreader.setCursor(0);
++ throw EntityArgument.ERROR_ONLY_PLAYERS_ALLOWED.createWithContext(stringreader);
+ } else {
+ return entityselector;
+ }
+@@ -129,7 +135,12 @@
+ StringReader stringreader = new StringReader(suggestionsbuilder.getInput());
+
+ stringreader.setCursor(suggestionsbuilder.getStart());
+- EntitySelectorParser argumentparserselector = new EntitySelectorParser(stringreader, EntitySelectorParser.allowSelectors(icompletionprovider));
++ // Paper start - Fix EntityArgument permissions
++ final boolean permission = object instanceof CommandSourceStack stack
++ ? stack.bypassSelectorPermissions || stack.hasPermission(2, "minecraft.command.selector")
++ : icompletionprovider.hasPermission(2);
++ EntitySelectorParser argumentparserselector = new EntitySelectorParser(stringreader, permission);
++ // Paper end - Fix EntityArgument permissions
+
+ try {
+ argumentparserselector.parse();
diff --git a/paper-server/patches/sources/net/minecraft/commands/arguments/MessageArgument.java.patch b/paper-server/patches/sources/net/minecraft/commands/arguments/MessageArgument.java.patch
new file mode 100644
index 0000000000..ba048d7f68
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/commands/arguments/MessageArgument.java.patch
@@ -0,0 +1,41 @@
+--- a/net/minecraft/commands/arguments/MessageArgument.java
++++ b/net/minecraft/commands/arguments/MessageArgument.java
+@@ -40,6 +40,11 @@
+
+ public static void resolveChatMessage(CommandContext context, String name, Consumer callback) throws CommandSyntaxException {
+ MessageArgument.Message message = context.getArgument(name, MessageArgument.Message.class);
++ // Paper start
++ resolveChatMessage(message, context, name, callback);
++ }
++ public static void resolveChatMessage(MessageArgument.Message message, CommandContext context, String name, Consumer callback) throws CommandSyntaxException {
++ // Paper end
+ CommandSourceStack commandSourceStack = context.getSource();
+ Component component = message.resolveComponent(commandSourceStack);
+ CommandSigningContext commandSigningContext = commandSourceStack.getSigningContext();
+@@ -54,17 +59,21 @@
+ private static void resolveSignedMessage(Consumer callback, CommandSourceStack source, PlayerChatMessage message) {
+ MinecraftServer minecraftServer = source.getServer();
+ CompletableFuture completableFuture = filterPlainText(source, message);
+- Component component = minecraftServer.getChatDecorator().decorate(source.getPlayer(), message.decoratedContent());
+- source.getChatMessageChainer().append(completableFuture, filtered -> {
+- PlayerChatMessage playerChatMessage2 = message.withUnsignedContent(component).filter(filtered.mask());
++ // Paper start - support asynchronous chat decoration
++ CompletableFuture componentFuture = minecraftServer.getChatDecorator().decorate(source.getPlayer(), source, message.decoratedContent());
++ source.getChatMessageChainer().append(CompletableFuture.allOf(completableFuture, componentFuture), filtered -> {
++ PlayerChatMessage playerChatMessage2 = message.withUnsignedContent(componentFuture.join()).filter(completableFuture.join().mask());
++ // Paper end - support asynchronous chat decoration
+ callback.accept(playerChatMessage2);
+ });
+ }
+
+ private static void resolveDisguisedMessage(Consumer callback, CommandSourceStack source, PlayerChatMessage message) {
+ ChatDecorator chatDecorator = source.getServer().getChatDecorator();
+- Component component = chatDecorator.decorate(source.getPlayer(), message.decoratedContent());
+- callback.accept(message.withUnsignedContent(component));
++ // Paper start - support asynchronous chat decoration
++ CompletableFuture componentFuture = chatDecorator.decorate(source.getPlayer(), source, message.decoratedContent());
++ source.getChatMessageChainer().append(componentFuture, (result) -> callback.accept(message.withUnsignedContent(result)));
++ // Paper end - support asynchronous chat decoration
+ }
+
+ private static CompletableFuture filterPlainText(CommandSourceStack source, PlayerChatMessage message) {
diff --git a/paper-server/patches/sources/net/minecraft/commands/arguments/blocks/BlockStateParser.java.patch b/paper-server/patches/sources/net/minecraft/commands/arguments/blocks/BlockStateParser.java.patch
new file mode 100644
index 0000000000..46961c4f64
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/commands/arguments/blocks/BlockStateParser.java.patch
@@ -0,0 +1,38 @@
+--- a/net/minecraft/commands/arguments/blocks/BlockStateParser.java
++++ b/net/minecraft/commands/arguments/blocks/BlockStateParser.java
+@@ -67,7 +67,7 @@
+ private final StringReader reader;
+ private final boolean forTesting;
+ private final boolean allowNbt;
+- private final Map, Comparable>> properties = Maps.newHashMap();
++ private final Map, Comparable>> properties = Maps.newLinkedHashMap(); // CraftBukkit - stable
+ private final Map vagueProperties = Maps.newHashMap();
+ private ResourceLocation id = ResourceLocation.withDefaultNamespace("");
+ @Nullable
+@@ -275,7 +275,7 @@
+ Iterator iterator = property.getPossibleValues().iterator();
+
+ while (iterator.hasNext()) {
+- T t0 = (Comparable) iterator.next();
++ T t0 = (T) iterator.next(); // CraftBukkit - decompile error
+
+ if (t0 instanceof Integer integer) {
+ builder.suggest(integer);
+@@ -545,7 +545,7 @@
+ Optional optional = property.getValue(value);
+
+ if (optional.isPresent()) {
+- this.state = (BlockState) this.state.setValue(property, (Comparable) optional.get());
++ this.state = (BlockState) this.state.setValue(property, (T) optional.get()); // CraftBukkit - decompile error
+ this.properties.put(property, (Comparable) optional.get());
+ } else {
+ this.reader.setCursor(cursor);
+@@ -581,7 +581,7 @@
+ private static > void appendProperty(StringBuilder builder, Property property, Comparable> value) {
+ builder.append(property.getName());
+ builder.append('=');
+- builder.append(property.getName(value));
++ builder.append(property.getName((T) value)); // CraftBukkit - decompile error
+ }
+
+ public static record BlockResult(BlockState blockState, Map, Comparable>> properties, @Nullable CompoundTag nbt) {
diff --git a/paper-server/patches/sources/net/minecraft/commands/arguments/item/ItemInput.java.patch b/paper-server/patches/sources/net/minecraft/commands/arguments/item/ItemInput.java.patch
new file mode 100644
index 0000000000..a20c412208
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/commands/arguments/item/ItemInput.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/commands/arguments/item/ItemInput.java
++++ b/net/minecraft/commands/arguments/item/ItemInput.java
+@@ -78,6 +78,6 @@
+ }
+
+ private String getItemName() {
+- return this.item.unwrapKey().map(ResourceKey::location).orElseGet(() -> "unknown[" + this.item + "]").toString();
++ return this.item.unwrapKey().map(ResourceKey::location).orElseGet(() -> "unknown[" + this.item + "]").toString(); // Paper - decompile fix
+ }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/commands/arguments/selector/EntitySelector.java.patch b/paper-server/patches/sources/net/minecraft/commands/arguments/selector/EntitySelector.java.patch
new file mode 100644
index 0000000000..cb23767a27
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/commands/arguments/selector/EntitySelector.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/commands/arguments/selector/EntitySelector.java
++++ b/net/minecraft/commands/arguments/selector/EntitySelector.java
+@@ -93,7 +93,7 @@
+ }
+
+ private void checkPermissions(CommandSourceStack source) throws CommandSyntaxException {
+- if (this.usesSelector && !source.hasPermission(2)) {
++ if (!source.bypassSelectorPermissions && (this.usesSelector && !source.hasPermission(2, "minecraft.command.selector"))) { // CraftBukkit // Paper - add bypass for selector perms
+ throw EntityArgument.ERROR_SELECTORS_NOT_ALLOWED.create();
+ }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/commands/arguments/selector/EntitySelectorParser.java.patch b/paper-server/patches/sources/net/minecraft/commands/arguments/selector/EntitySelectorParser.java.patch
new file mode 100644
index 0000000000..088ffce992
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/commands/arguments/selector/EntitySelectorParser.java.patch
@@ -0,0 +1,55 @@
+--- a/net/minecraft/commands/arguments/selector/EntitySelectorParser.java
++++ b/net/minecraft/commands/arguments/selector/EntitySelectorParser.java
+@@ -133,7 +133,7 @@
+ boolean flag;
+
+ if (source instanceof SharedSuggestionProvider icompletionprovider) {
+- if (icompletionprovider.hasPermission(2)) {
++ if (source instanceof net.minecraft.commands.CommandSourceStack stack ? stack.bypassSelectorPermissions || stack.hasPermission(2, "minecraft.command.selector") : icompletionprovider.hasPermission(2)) { // Paper - Fix EntityArgument permissions
+ flag = true;
+ return flag;
+ }
+@@ -158,7 +158,7 @@
+ axisalignedbb = this.createAabb(this.deltaX == null ? 0.0D : this.deltaX, this.deltaY == null ? 0.0D : this.deltaY, this.deltaZ == null ? 0.0D : this.deltaZ);
+ }
+
+- Function function;
++ Function function; // CraftBukkit - decompile error
+
+ if (this.x == null && this.y == null && this.z == null) {
+ function = (vec3d) -> {
+@@ -215,8 +215,10 @@
+ };
+ }
+
+- protected void parseSelector() throws CommandSyntaxException {
+- this.usesSelectors = true;
++ // CraftBukkit start
++ protected void parseSelector(boolean overridePermissions) throws CommandSyntaxException {
++ this.usesSelectors = !overridePermissions;
++ // CraftBukkit end
+ this.suggestions = this::suggestSelector;
+ if (!this.reader.canRead()) {
+ throw EntitySelectorParser.ERROR_MISSING_SELECTOR_TYPE.createWithContext(this.reader);
+@@ -505,6 +507,12 @@
+ }
+
+ public EntitySelector parse() throws CommandSyntaxException {
++ // CraftBukkit start
++ return this.parse(false);
++ }
++
++ public EntitySelector parse(boolean overridePermissions) throws CommandSyntaxException {
++ // CraftBukkit end
+ this.startPosition = this.reader.getCursor();
+ this.suggestions = this::suggestNameOrSelector;
+ if (this.reader.canRead() && this.reader.peek() == '@') {
+@@ -513,7 +521,7 @@
+ }
+
+ this.reader.skip();
+- this.parseSelector();
++ this.parseSelector(overridePermissions); // CraftBukkit
+ } else {
+ this.parseNameOrUUID();
+ }
diff --git a/paper-server/patches/sources/net/minecraft/commands/arguments/selector/SelectorPattern.java.patch b/paper-server/patches/sources/net/minecraft/commands/arguments/selector/SelectorPattern.java.patch
new file mode 100644
index 0000000000..ed6f4da14b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/commands/arguments/selector/SelectorPattern.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/commands/arguments/selector/SelectorPattern.java
++++ b/net/minecraft/commands/arguments/selector/SelectorPattern.java
+@@ -13,7 +13,7 @@
+ EntitySelectorParser entitySelectorParser = new EntitySelectorParser(new StringReader(selector), true);
+ return DataResult.success(new SelectorPattern(selector, entitySelectorParser.parse()));
+ } catch (CommandSyntaxException var2) {
+- return DataResult.error(() -> "Invalid selector component: " + selector + ": " + var2.getMessage());
++ return DataResult.error(() -> "Invalid selector component"); // Paper - limit selector error message
+ }
+ }
+
diff --git a/paper-server/patches/sources/net/minecraft/core/BlockPos.java.patch b/paper-server/patches/sources/net/minecraft/core/BlockPos.java.patch
new file mode 100644
index 0000000000..7858f6cef1
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/BlockPos.java.patch
@@ -0,0 +1,186 @@
+--- a/net/minecraft/core/BlockPos.java
++++ b/net/minecraft/core/BlockPos.java
+@@ -158,67 +158,84 @@
+
+ @Override
+ public BlockPos above() {
+- return this.relative(Direction.UP);
++ return new BlockPos(this.getX(), this.getY() + 1, this.getZ()); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos above(int distance) {
+- return this.relative(Direction.UP, distance);
++ return distance == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY() + distance, this.getZ()); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos below() {
+- return this.relative(Direction.DOWN);
++ return new BlockPos(this.getX(), this.getY() - 1, this.getZ()); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos below(int i) {
+- return this.relative(Direction.DOWN, i);
++ return i == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY() - i, this.getZ()); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos north() {
+- return this.relative(Direction.NORTH);
++ return new BlockPos(this.getX(), this.getY(), this.getZ() - 1); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos north(int distance) {
+- return this.relative(Direction.NORTH, distance);
++ return distance == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY(), this.getZ() - distance); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos south() {
+- return this.relative(Direction.SOUTH);
++ return new BlockPos(this.getX(), this.getY(), this.getZ() + 1); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos south(int distance) {
+- return this.relative(Direction.SOUTH, distance);
++ return distance == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY(), this.getZ() + distance); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos west() {
+- return this.relative(Direction.WEST);
++ return new BlockPos(this.getX() - 1, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos west(int distance) {
+- return this.relative(Direction.WEST, distance);
++ return distance == 0 ? this.immutable() : new BlockPos(this.getX() - distance, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos east() {
+- return this.relative(Direction.EAST);
++ return new BlockPos(this.getX() + 1, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos east(int distance) {
+- return this.relative(Direction.EAST, distance);
++ return distance == 0 ? this.immutable() : new BlockPos(this.getX() + distance, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos relative(Direction direction) {
++ // Paper start - Perf: Optimize BlockPosition
++ switch(direction) {
++ case UP:
++ return new BlockPos(this.getX(), this.getY() + 1, this.getZ());
++ case DOWN:
++ return new BlockPos(this.getX(), this.getY() - 1, this.getZ());
++ case NORTH:
++ return new BlockPos(this.getX(), this.getY(), this.getZ() - 1);
++ case SOUTH:
++ return new BlockPos(this.getX(), this.getY(), this.getZ() + 1);
++ case WEST:
++ return new BlockPos(this.getX() - 1, this.getY(), this.getZ());
++ case EAST:
++ return new BlockPos(this.getX() + 1, this.getY(), this.getZ());
++ default:
+ return new BlockPos(this.getX() + direction.getStepX(), this.getY() + direction.getStepY(), this.getZ() + direction.getStepZ());
++ }
++ // Paper end - Perf: Optimize BlockPosition
+ }
+
+ @Override
+@@ -324,9 +341,11 @@
+
+ public static Iterable withinManhattan(BlockPos center, int rangeX, int rangeY, int rangeZ) {
+ int i = rangeX + rangeY + rangeZ;
+- int j = center.getX();
+- int k = center.getY();
+- int l = center.getZ();
++ // Paper start - rename variables to fix conflict with anonymous class (remap fix)
++ int centerX = center.getX();
++ int centerY = center.getY();
++ int centerZ = center.getZ();
++ // Paper end
+ return () -> new AbstractIterator() {
+ private final BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
+ private int currentDepth;
+@@ -340,7 +359,7 @@
+ protected BlockPos computeNext() {
+ if (this.zMirror) {
+ this.zMirror = false;
+- this.cursor.setZ(l - (this.cursor.getZ() - l));
++ this.cursor.setZ(centerZ - (this.cursor.getZ() - centerZ)); // Paper - remap fix
+ return this.cursor;
+ } else {
+ BlockPos blockPos;
+@@ -366,7 +385,7 @@
+ int k = this.currentDepth - Math.abs(i) - Math.abs(j);
+ if (k <= rangeZ) {
+ this.zMirror = k != 0;
+- blockPos = this.cursor.set(j + i, k + j, l + k);
++ blockPos = this.cursor.set(centerX + i, centerY + j, centerZ + k); // Paper - remap fix
+ }
+ }
+
+@@ -444,12 +463,12 @@
+ if (this.index == l) {
+ return this.endOfData();
+ } else {
+- int i = this.index % i;
+- int j = this.index / i;
+- int k = j % j;
+- int l = j / j;
++ int offsetX = this.index % i; // Paper - decomp fix
++ int u = this.index / i; // Paper - decomp fix
++ int offsetY = u % j; // Paper - decomp fix
++ int offsetZ = u / j; // Paper - decomp fix
+ this.index++;
+- return this.cursor.set(startX + i, startY + k, startZ + l);
++ return this.cursor.set(startX + offsetX, startY + offsetY, startZ + offsetZ); // Paper - decomp fix
+ }
+ }
+ };
+@@ -569,9 +588,9 @@
+ }
+
+ public BlockPos.MutableBlockPos set(int x, int y, int z) {
+- this.setX(x);
+- this.setY(y);
+- this.setZ(z);
++ this.x = x; // Paper - Perf: Manually inline methods in BlockPosition
++ this.y = y; // Paper - Perf: Manually inline methods in BlockPosition
++ this.z = z; // Paper - Perf: Manually inline methods in BlockPosition
+ return this;
+ }
+
+@@ -636,19 +655,19 @@
+
+ @Override
+ public BlockPos.MutableBlockPos setX(int i) {
+- super.setX(i);
++ this.x = i; // Paper - Perf: Manually inline methods in BlockPosition
+ return this;
+ }
+
+ @Override
+ public BlockPos.MutableBlockPos setY(int i) {
+- super.setY(i);
++ this.y = i; // Paper - Perf: Manually inline methods in BlockPosition
+ return this;
+ }
+
+ @Override
+ public BlockPos.MutableBlockPos setZ(int i) {
+- super.setZ(i);
++ this.z = i; // Paper - Perf: Manually inline methods in BlockPosition
+ return this;
+ }
+
diff --git a/paper-server/patches/sources/net/minecraft/core/Direction.java.patch b/paper-server/patches/sources/net/minecraft/core/Direction.java.patch
new file mode 100644
index 0000000000..a0a77c5f10
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/Direction.java.patch
@@ -0,0 +1,46 @@
+--- a/net/minecraft/core/Direction.java
++++ b/net/minecraft/core/Direction.java
+@@ -57,6 +57,12 @@
+ .sorted(Comparator.comparingInt(direction -> direction.data2d))
+ .toArray(Direction[]::new);
+
++ // Paper start - Perf: Inline shift direction fields
++ private final int adjX;
++ private final int adjY;
++ private final int adjZ;
++ // Paper end - Perf: Inline shift direction fields
++
+ private Direction(
+ final int id,
+ final int idOpposite,
+@@ -74,6 +80,11 @@
+ this.axisDirection = direction;
+ this.normal = vector;
+ this.normalVec3 = Vec3.atLowerCornerOf(vector);
++ // Paper start - Perf: Inline shift direction fields
++ this.adjX = vector.getX();
++ this.adjY = vector.getY();
++ this.adjZ = vector.getZ();
++ // Paper end - Perf: Inline shift direction fields
+ }
+
+ public static Direction[] orderedByNearest(Entity entity) {
+@@ -247,15 +258,15 @@
+ }
+
+ public int getStepX() {
+- return this.normal.getX();
++ return this.adjX; // Paper - Perf: Inline shift direction fields
+ }
+
+ public int getStepY() {
+- return this.normal.getY();
++ return this.adjY; // Paper - Perf: Inline shift direction fields
+ }
+
+ public int getStepZ() {
+- return this.normal.getZ();
++ return this.adjZ; // Paper - Perf: Inline shift direction fields
+ }
+
+ public Vector3f step() {
diff --git a/paper-server/patches/sources/net/minecraft/core/Holder.java.patch b/paper-server/patches/sources/net/minecraft/core/Holder.java.patch
new file mode 100644
index 0000000000..634a2b2190
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/Holder.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/core/Holder.java
++++ b/net/minecraft/core/Holder.java
+@@ -230,7 +230,7 @@
+ }
+
+ void bindTags(Collection> tags) {
+- this.tags = Set.copyOf(tags);
++ this.tags = java.util.Collections.unmodifiableSet(new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(tags)); // Paper
+ }
+
+ @Override
diff --git a/paper-server/patches/sources/net/minecraft/core/MappedRegistry.java.patch b/paper-server/patches/sources/net/minecraft/core/MappedRegistry.java.patch
new file mode 100644
index 0000000000..4d3c8b552e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/MappedRegistry.java.patch
@@ -0,0 +1,33 @@
+--- a/net/minecraft/core/MappedRegistry.java
++++ b/net/minecraft/core/MappedRegistry.java
+@@ -33,11 +33,11 @@
+ public class MappedRegistry implements WritableRegistry {
+ private final ResourceKey extends Registry> key;
+ private final ObjectList> byId = new ObjectArrayList<>(256);
+- private final Reference2IntMap toId = Util.make(new Reference2IntOpenHashMap<>(), map -> map.defaultReturnValue(-1));
+- private final Map> byLocation = new HashMap<>();
+- private final Map, Holder.Reference> byKey = new HashMap<>();
+- private final Map> byValue = new IdentityHashMap<>();
+- private final Map, RegistrationInfo> registrationInfos = new IdentityHashMap<>();
++ private final Reference2IntMap toId = Util.make(new Reference2IntOpenHashMap<>(2048), map -> map.defaultReturnValue(-1)); // Paper - Perf: Use bigger expected size to reduce collisions
++ private final Map> byLocation = new HashMap<>(2048); // Paper - Perf: Use bigger expected size to reduce collisions
++ private final Map, Holder.Reference> byKey = new HashMap<>(2048); // Paper - Perf: Use bigger expected size to reduce collisions
++ private final Map> byValue = new IdentityHashMap<>(2048); // Paper - Perf: Use bigger expected size to reduce collisions
++ private final Map, RegistrationInfo> registrationInfos = new IdentityHashMap<>(2048); // Paper - Perf: Use bigger expected size to reduce collisions
+ private Lifecycle registryLifecycle;
+ private final Map, HolderSet.Named> frozenTags = new IdentityHashMap<>();
+ MappedRegistry.TagSet allTags = MappedRegistry.TagSet.unbound();
+@@ -508,5 +508,13 @@
+ void forEach(BiConsumer super TagKey, ? super HolderSet.Named> consumer);
+
+ Stream> getTags();
++ }
++ // Paper start
++ // used to clear intrusive holders from GameEvent, Item, Block, EntityType, and Fluid from unused instances of those types
++ public void clearIntrusiveHolder(final T instance) {
++ if (this.unregisteredIntrusiveHolders != null) {
++ this.unregisteredIntrusiveHolders.remove(instance);
++ }
+ }
++ // Paper end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/core/Rotations.java.patch b/paper-server/patches/sources/net/minecraft/core/Rotations.java.patch
new file mode 100644
index 0000000000..29c5bfc220
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/Rotations.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/core/Rotations.java
++++ b/net/minecraft/core/Rotations.java
+@@ -34,6 +34,18 @@
+ this(serialized.getFloat(0), serialized.getFloat(1), serialized.getFloat(2));
+ }
+
++ // Paper start - faster alternative constructor
++ private Rotations(float x, float y, float z, Void dummy_var) {
++ this.x = x;
++ this.y = y;
++ this.z = z;
++ }
++
++ public static Rotations createWithoutValidityChecks(float x, float y, float z) {
++ return new Rotations(x, y, z, null);
++ }
++ // Paper end - faster alternative constructor
++
+ public ListTag save() {
+ ListTag listTag = new ListTag();
+ listTag.add(FloatTag.valueOf(this.x));
diff --git a/paper-server/patches/sources/net/minecraft/core/Vec3i.java.patch b/paper-server/patches/sources/net/minecraft/core/Vec3i.java.patch
new file mode 100644
index 0000000000..8c19b63ae6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/Vec3i.java.patch
@@ -0,0 +1,49 @@
+--- a/net/minecraft/core/Vec3i.java
++++ b/net/minecraft/core/Vec3i.java
+@@ -16,9 +16,9 @@
+ vec -> IntStream.of(vec.getX(), vec.getY(), vec.getZ())
+ );
+ public static final Vec3i ZERO = new Vec3i(0, 0, 0);
+- private int x;
+- private int y;
+- private int z;
++ protected int x; // Paper - Perf: Manually inline methods in BlockPosition; protected
++ protected int y; // Paper - Perf: Manually inline methods in BlockPosition; protected
++ protected int z; // Paper - Perf: Manually inline methods in BlockPosition; protected
+
+ public static Codec offsetCodec(int maxAbsValue) {
+ return CODEC.validate(
+@@ -35,12 +35,12 @@
+ }
+
+ @Override
+- public boolean equals(Object object) {
++ public final boolean equals(Object object) { // Paper - Perf: Final for inline
+ return this == object || object instanceof Vec3i vec3i && this.getX() == vec3i.getX() && this.getY() == vec3i.getY() && this.getZ() == vec3i.getZ();
+ }
+
+ @Override
+- public int hashCode() {
++ public final int hashCode() { // Paper - Perf: Final for inline
+ return (this.getY() + this.getZ() * 31) * 31 + this.getX();
+ }
+
+@@ -53,15 +53,15 @@
+ }
+ }
+
+- public int getX() {
++ public final int getX() { // Paper - Perf: Final for inline
+ return this.x;
+ }
+
+- public int getY() {
++ public final int getY() { // Paper - Perf: Final for inline
+ return this.y;
+ }
+
+- public int getZ() {
++ public final int getZ() { // Paper - Perf: Final for inline
+ return this.z;
+ }
+
diff --git a/paper-server/patches/sources/net/minecraft/core/cauldron/CauldronInteraction.java.patch b/paper-server/patches/sources/net/minecraft/core/cauldron/CauldronInteraction.java.patch
new file mode 100644
index 0000000000..f0db760867
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/cauldron/CauldronInteraction.java.patch
@@ -0,0 +1,330 @@
+--- a/net/minecraft/core/cauldron/CauldronInteraction.java
++++ b/net/minecraft/core/cauldron/CauldronInteraction.java
+@@ -35,20 +35,25 @@
+ import net.minecraft.world.level.block.state.BlockState;
+ import net.minecraft.world.level.gameevent.GameEvent;
+ import net.minecraft.world.level.material.FluidState;
++// CraftBukkit start
++import org.bukkit.event.block.CauldronLevelChangeEvent;
++// CraftBukkit end
+
+ public interface CauldronInteraction {
+
+ Map INTERACTIONS = new Object2ObjectArrayMap();
+- Codec CODEC;
+- CauldronInteraction.InteractionMap EMPTY;
+- CauldronInteraction.InteractionMap WATER;
+- CauldronInteraction.InteractionMap LAVA;
+- CauldronInteraction.InteractionMap POWDER_SNOW;
++ // CraftBukkit start - decompile errors
++ Codec CODEC = Codec.stringResolver(CauldronInteraction.InteractionMap::name, CauldronInteraction.INTERACTIONS::get);
++ CauldronInteraction.InteractionMap EMPTY = CauldronInteraction.newInteractionMap("empty");
++ CauldronInteraction.InteractionMap WATER = CauldronInteraction.newInteractionMap("water");
++ CauldronInteraction.InteractionMap LAVA = CauldronInteraction.newInteractionMap("lava");
++ CauldronInteraction.InteractionMap POWDER_SNOW = CauldronInteraction.newInteractionMap("powder_snow");
++ // CraftBukkit end
+
+ static CauldronInteraction.InteractionMap newInteractionMap(String name) {
+ Object2ObjectOpenHashMap- object2objectopenhashmap = new Object2ObjectOpenHashMap();
+
+- object2objectopenhashmap.defaultReturnValue((iblockdata, world, blockposition, entityhuman, enumhand, itemstack) -> {
++ object2objectopenhashmap.defaultReturnValue((iblockdata, world, blockposition, entityhuman, enumhand, itemstack, hitDirection) -> { // Paper - add hitDirection
+ return InteractionResult.TRY_WITH_EMPTY_HAND;
+ });
+ CauldronInteraction.InteractionMap cauldroninteraction_a = new CauldronInteraction.InteractionMap(name, object2objectopenhashmap);
+@@ -57,23 +62,28 @@
+ return cauldroninteraction_a;
+ }
+
+- InteractionResult interact(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack);
++ InteractionResult interact(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection); // Paper - add hitDirection
+
+ static void bootStrap() {
+ Map
- map = CauldronInteraction.EMPTY.map();
+
+ CauldronInteraction.addDefaultInteractions(map);
+- map.put(Items.POTION, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack) -> {
++ map.put(Items.POTION, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack, hitDirection) -> { // Paper - add hitDirection
+ PotionContents potioncontents = (PotionContents) itemstack.get(DataComponents.POTION_CONTENTS);
+
+ if (potioncontents != null && potioncontents.is(Potions.WATER)) {
+ if (!world.isClientSide) {
++ // CraftBukkit start
++ if (!LayeredCauldronBlock.changeLevel(iblockdata, world, blockposition, Blocks.WATER_CAULDRON.defaultBlockState(), entityhuman, CauldronLevelChangeEvent.ChangeReason.BOTTLE_EMPTY, false)) { // Paper - Call CauldronLevelChangeEvent
++ return InteractionResult.SUCCESS;
++ }
++ // CraftBukkit end
+ Item item = itemstack.getItem();
+
+ entityhuman.setItemInHand(enumhand, ItemUtils.createFilledResult(itemstack, entityhuman, new ItemStack(Items.GLASS_BOTTLE)));
+ entityhuman.awardStat(Stats.USE_CAULDRON);
+ entityhuman.awardStat(Stats.ITEM_USED.get(item));
+- world.setBlockAndUpdate(blockposition, Blocks.WATER_CAULDRON.defaultBlockState());
++ // world.setBlockAndUpdate(blockposition, Blocks.WATER_CAULDRON.defaultBlockState()); // CraftBukkit
+ world.playSound((Player) null, blockposition, SoundEvents.BOTTLE_EMPTY, SoundSource.BLOCKS, 1.0F, 1.0F);
+ world.gameEvent((Entity) null, (Holder) GameEvent.FLUID_PLACE, blockposition);
+ }
+@@ -86,26 +96,31 @@
+ Map
- map1 = CauldronInteraction.WATER.map();
+
+ CauldronInteraction.addDefaultInteractions(map1);
+- map1.put(Items.BUCKET, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack) -> {
++ map1.put(Items.BUCKET, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack, hitDirection) -> { // Paper - add hitDirection
+ return CauldronInteraction.fillBucket(iblockdata, world, blockposition, entityhuman, enumhand, itemstack, new ItemStack(Items.WATER_BUCKET), (iblockdata1) -> {
+ return (Integer) iblockdata1.getValue(LayeredCauldronBlock.LEVEL) == 3;
+- }, SoundEvents.BUCKET_FILL);
++ }, SoundEvents.BUCKET_FILL, hitDirection); // Paper - add hitDirection
+ });
+- map1.put(Items.GLASS_BOTTLE, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack) -> {
++ map1.put(Items.GLASS_BOTTLE, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack, hitDirection) -> { // Paper - add hitDirection
+ if (!world.isClientSide) {
++ // CraftBukkit start
++ if (!LayeredCauldronBlock.lowerFillLevel(iblockdata, world, blockposition, entityhuman, CauldronLevelChangeEvent.ChangeReason.BOTTLE_FILL)) {
++ return InteractionResult.SUCCESS;
++ }
++ // CraftBukkit end
+ Item item = itemstack.getItem();
+
+ entityhuman.setItemInHand(enumhand, ItemUtils.createFilledResult(itemstack, entityhuman, PotionContents.createItemStack(Items.POTION, Potions.WATER)));
+ entityhuman.awardStat(Stats.USE_CAULDRON);
+ entityhuman.awardStat(Stats.ITEM_USED.get(item));
+- LayeredCauldronBlock.lowerFillLevel(iblockdata, world, blockposition);
++ // LayeredCauldronBlock.lowerFillLevel(iblockdata, world, blockposition); // CraftBukkit
+ world.playSound((Player) null, blockposition, SoundEvents.BOTTLE_FILL, SoundSource.BLOCKS, 1.0F, 1.0F);
+ world.gameEvent((Entity) null, (Holder) GameEvent.FLUID_PICKUP, blockposition);
+ }
+
+ return InteractionResult.SUCCESS;
+ });
+- map1.put(Items.POTION, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack) -> {
++ map1.put(Items.POTION, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack, hitDirection) -> { // Paper - add hitDirection
+ if ((Integer) iblockdata.getValue(LayeredCauldronBlock.LEVEL) == 3) {
+ return InteractionResult.TRY_WITH_EMPTY_HAND;
+ } else {
+@@ -113,10 +128,15 @@
+
+ if (potioncontents != null && potioncontents.is(Potions.WATER)) {
+ if (!world.isClientSide) {
++ // CraftBukkit start
++ if (!LayeredCauldronBlock.changeLevel(iblockdata, world, blockposition, iblockdata.cycle(LayeredCauldronBlock.LEVEL), entityhuman, CauldronLevelChangeEvent.ChangeReason.BOTTLE_EMPTY, false)) { // Paper - Call CauldronLevelChangeEvent
++ return InteractionResult.SUCCESS;
++ }
++ // CraftBukkit end
+ entityhuman.setItemInHand(enumhand, ItemUtils.createFilledResult(itemstack, entityhuman, new ItemStack(Items.GLASS_BOTTLE)));
+ entityhuman.awardStat(Stats.USE_CAULDRON);
+ entityhuman.awardStat(Stats.ITEM_USED.get(itemstack.getItem()));
+- world.setBlockAndUpdate(blockposition, (BlockState) iblockdata.cycle(LayeredCauldronBlock.LEVEL));
++ // world.setBlockAndUpdate(blockposition, (IBlockData) iblockdata.cycle(LayeredCauldronBlock.LEVEL)); // CraftBukkit
+ world.playSound((Player) null, blockposition, SoundEvents.BOTTLE_EMPTY, SoundSource.BLOCKS, 1.0F, 1.0F);
+ world.gameEvent((Entity) null, (Holder) GameEvent.FLUID_PLACE, blockposition);
+ }
+@@ -167,18 +187,18 @@
+ map1.put(Items.YELLOW_SHULKER_BOX, CauldronInteraction::shulkerBoxInteraction);
+ Map
- map2 = CauldronInteraction.LAVA.map();
+
+- map2.put(Items.BUCKET, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack) -> {
++ map2.put(Items.BUCKET, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack, hitDirection) -> { // Paper - add hitDirection
+ return CauldronInteraction.fillBucket(iblockdata, world, blockposition, entityhuman, enumhand, itemstack, new ItemStack(Items.LAVA_BUCKET), (iblockdata1) -> {
+ return true;
+- }, SoundEvents.BUCKET_FILL_LAVA);
++ }, SoundEvents.BUCKET_FILL_LAVA, hitDirection); // Paper - add hitDirection
+ });
+ CauldronInteraction.addDefaultInteractions(map2);
+ Map
- map3 = CauldronInteraction.POWDER_SNOW.map();
+
+- map3.put(Items.BUCKET, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack) -> {
++ map3.put(Items.BUCKET, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack, hitDirection) -> { // Paper - add hitDirection
+ return CauldronInteraction.fillBucket(iblockdata, world, blockposition, entityhuman, enumhand, itemstack, new ItemStack(Items.POWDER_SNOW_BUCKET), (iblockdata1) -> {
+ return (Integer) iblockdata1.getValue(LayeredCauldronBlock.LEVEL) == 3;
+- }, SoundEvents.BUCKET_FILL_POWDER_SNOW);
++ }, SoundEvents.BUCKET_FILL_POWDER_SNOW, hitDirection); // Paper - add hitDirection
+ });
+ CauldronInteraction.addDefaultInteractions(map3);
+ }
+@@ -190,16 +210,35 @@
+ }
+
+ static InteractionResult fillBucket(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, ItemStack output, Predicate
fullPredicate, SoundEvent soundEvent) {
++ // Paper start - add hitDirection
++ return fillBucket(state, world, pos, player, hand, stack, output, fullPredicate, soundEvent, null); // Paper - add hitDirection
++ }
++ static InteractionResult fillBucket(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, ItemStack output, Predicate fullPredicate, SoundEvent soundEvent, @javax.annotation.Nullable net.minecraft.core.Direction hitDirection) {
++ // Paper end - add hitDirection
+ if (!fullPredicate.test(state)) {
+ return InteractionResult.TRY_WITH_EMPTY_HAND;
+ } else {
+ if (!world.isClientSide) {
++ // Paper start - fire PlayerBucketFillEvent
++ if (hitDirection != null) {
++ org.bukkit.event.player.PlayerBucketEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBucketFillEvent((net.minecraft.server.level.ServerLevel) world, player, pos, pos, hitDirection, stack, output.getItem(), hand);
++ if (event.isCancelled()) {
++ return InteractionResult.PASS;
++ }
++ output = event.getItemStack() != null ? org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItemStack()) : ItemStack.EMPTY;
++ }
++ // Paper end - fire PlayerBucketFillEvent
++ // CraftBukkit start
++ if (!LayeredCauldronBlock.changeLevel(state, world, pos, Blocks.CAULDRON.defaultBlockState(), player, CauldronLevelChangeEvent.ChangeReason.BUCKET_FILL, false)) { // Paper - Call CauldronLevelChangeEvent
++ return InteractionResult.SUCCESS;
++ }
++ // CraftBukkit end
+ Item item = stack.getItem();
+
+ player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, output));
+ player.awardStat(Stats.USE_CAULDRON);
+ player.awardStat(Stats.ITEM_USED.get(item));
+- world.setBlockAndUpdate(pos, Blocks.CAULDRON.defaultBlockState());
++ // world.setBlockAndUpdate(blockposition, Blocks.CAULDRON.defaultBlockState()); // CraftBukkit
+ world.playSound((Player) null, pos, soundEvent, SoundSource.BLOCKS, 1.0F, 1.0F);
+ world.gameEvent((Entity) null, (Holder) GameEvent.FLUID_PICKUP, pos);
+ }
+@@ -209,13 +248,33 @@
+ }
+
+ static InteractionResult emptyBucket(Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, BlockState state, SoundEvent soundEvent) {
++ // Paper start - add hitDirection
++ return emptyBucket(world, pos, player, hand, stack, state, soundEvent, null);
++ }
++ static InteractionResult emptyBucket(Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, BlockState state, SoundEvent soundEvent, @javax.annotation.Nullable net.minecraft.core.Direction hitDirection) {
++ // Paper end - add hitDirection
+ if (!world.isClientSide) {
++ // Paper start - fire PlayerBucketEmptyEvent
++ ItemStack output = new ItemStack(Items.BUCKET);
++ if (hitDirection != null) {
++ org.bukkit.event.player.PlayerBucketEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBucketEmptyEvent((net.minecraft.server.level.ServerLevel) world, player, pos, pos, hitDirection, stack, hand);
++ if (event.isCancelled()) {
++ return InteractionResult.PASS;
++ }
++ output = event.getItemStack() != null ? org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItemStack()) : ItemStack.EMPTY;
++ }
++ // Paper end - fire PlayerBucketEmptyEvent
++ // CraftBukkit start
++ if (!LayeredCauldronBlock.changeLevel(state, world, pos, state, player, CauldronLevelChangeEvent.ChangeReason.BUCKET_EMPTY, false)) { // Paper - Call CauldronLevelChangeEvent
++ return InteractionResult.SUCCESS;
++ }
++ // CraftBukkit end
+ Item item = stack.getItem();
+
+- player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, new ItemStack(Items.BUCKET)));
++ player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, output)); // Paper
+ player.awardStat(Stats.FILL_CAULDRON);
+ player.awardStat(Stats.ITEM_USED.get(item));
+- world.setBlockAndUpdate(pos, state);
++ // world.setBlockAndUpdate(blockposition, iblockdata); // CraftBukkit
+ world.playSound((Player) null, pos, soundEvent, SoundSource.BLOCKS, 1.0F, 1.0F);
+ world.gameEvent((Entity) null, (Holder) GameEvent.FLUID_PLACE, pos);
+ }
+@@ -223,65 +282,79 @@
+ return InteractionResult.SUCCESS;
+ }
+
+- private static InteractionResult fillWaterInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
+- return CauldronInteraction.emptyBucket(world, pos, player, hand, stack, (BlockState) Blocks.WATER_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, 3), SoundEvents.BUCKET_EMPTY);
++ private static InteractionResult fillWaterInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
++ return CauldronInteraction.emptyBucket(world, pos, player, hand, stack, (BlockState) Blocks.WATER_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, 3), SoundEvents.BUCKET_EMPTY, hitDirection); // Paper - add hitDirection
+ }
+
+- private static InteractionResult fillLavaInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
+- return (InteractionResult) (CauldronInteraction.isUnderWater(world, pos) ? InteractionResult.CONSUME : CauldronInteraction.emptyBucket(world, pos, player, hand, stack, Blocks.LAVA_CAULDRON.defaultBlockState(), SoundEvents.BUCKET_EMPTY_LAVA));
++ private static InteractionResult fillLavaInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
++ return (InteractionResult) (CauldronInteraction.isUnderWater(world, pos) ? InteractionResult.CONSUME : CauldronInteraction.emptyBucket(world, pos, player, hand, stack, Blocks.LAVA_CAULDRON.defaultBlockState(), SoundEvents.BUCKET_EMPTY_LAVA, hitDirection)); // Paper - add hitDirection
+ }
+
+- private static InteractionResult fillPowderSnowInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
+- return (InteractionResult) (CauldronInteraction.isUnderWater(world, pos) ? InteractionResult.CONSUME : CauldronInteraction.emptyBucket(world, pos, player, hand, stack, (BlockState) Blocks.POWDER_SNOW_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, 3), SoundEvents.BUCKET_EMPTY_POWDER_SNOW));
++ private static InteractionResult fillPowderSnowInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
++ return (InteractionResult) (CauldronInteraction.isUnderWater(world, pos) ? InteractionResult.CONSUME : CauldronInteraction.emptyBucket(world, pos, player, hand, stack, (BlockState) Blocks.POWDER_SNOW_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, 3), SoundEvents.BUCKET_EMPTY_POWDER_SNOW, hitDirection)); // Paper - add hitDirection
+ }
+
+- private static InteractionResult shulkerBoxInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
++ private static InteractionResult shulkerBoxInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
+ Block block = Block.byItem(stack.getItem());
+
+ if (!(block instanceof ShulkerBoxBlock)) {
+ return InteractionResult.TRY_WITH_EMPTY_HAND;
+ } else {
+ if (!world.isClientSide) {
++ // CraftBukkit start
++ if (!LayeredCauldronBlock.lowerFillLevel(state, world, pos, player, CauldronLevelChangeEvent.ChangeReason.SHULKER_WASH)) {
++ return InteractionResult.SUCCESS;
++ }
++ // CraftBukkit end
+ ItemStack itemstack1 = stack.transmuteCopy(Blocks.SHULKER_BOX, 1);
+
+ player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, itemstack1, false));
+ player.awardStat(Stats.CLEAN_SHULKER_BOX);
+- LayeredCauldronBlock.lowerFillLevel(state, world, pos);
++ // LayeredCauldronBlock.lowerFillLevel(iblockdata, world, blockposition); // CraftBukkit
+ }
+-
+ return InteractionResult.SUCCESS;
+ }
+ }
+
+- private static InteractionResult bannerInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
++ private static InteractionResult bannerInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
+ BannerPatternLayers bannerpatternlayers = (BannerPatternLayers) stack.getOrDefault(DataComponents.BANNER_PATTERNS, BannerPatternLayers.EMPTY);
+
+ if (bannerpatternlayers.layers().isEmpty()) {
+ return InteractionResult.TRY_WITH_EMPTY_HAND;
+ } else {
+ if (!world.isClientSide) {
++ // CraftBukkit start
++ if (!LayeredCauldronBlock.lowerFillLevel(state, world, pos, player, CauldronLevelChangeEvent.ChangeReason.BANNER_WASH)) {
++ return InteractionResult.SUCCESS;
++ }
++ // CraftBukkit end
+ ItemStack itemstack1 = stack.copyWithCount(1);
+
+ itemstack1.set(DataComponents.BANNER_PATTERNS, bannerpatternlayers.removeLast());
+ player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, itemstack1, false));
+ player.awardStat(Stats.CLEAN_BANNER);
+- LayeredCauldronBlock.lowerFillLevel(state, world, pos);
++ // LayeredCauldronBlock.lowerFillLevel(iblockdata, world, blockposition); // CraftBukkit
+ }
+
+ return InteractionResult.SUCCESS;
+ }
+ }
+
+- private static InteractionResult dyedItemIteration(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
++ private static InteractionResult dyedItemIteration(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
+ if (!stack.is(ItemTags.DYEABLE)) {
+ return InteractionResult.TRY_WITH_EMPTY_HAND;
+ } else if (!stack.has(DataComponents.DYED_COLOR)) {
+ return InteractionResult.TRY_WITH_EMPTY_HAND;
+ } else {
+ if (!world.isClientSide) {
++ // CraftBukkit start
++ if (!LayeredCauldronBlock.lowerFillLevel(state, world, pos, player, CauldronLevelChangeEvent.ChangeReason.ARMOR_WASH)) {
++ return InteractionResult.SUCCESS;
++ }
++ // CraftBukkit end
+ stack.remove(DataComponents.DYED_COLOR);
+ player.awardStat(Stats.CLEAN_ARMOR);
+- LayeredCauldronBlock.lowerFillLevel(state, world, pos);
++ // LayeredCauldronBlock.lowerFillLevel(iblockdata, world, blockposition); // CraftBukkit
+ }
+
+ return InteractionResult.SUCCESS;
+@@ -294,8 +367,10 @@
+ return fluid.is(FluidTags.WATER);
+ }
+
++ // CraftBukkit start - decompile errors
++ /*
+ static {
+- Function function = CauldronInteraction.InteractionMap::name;
++ Function function = CauldronInteraction.a::name;
+ Map map = CauldronInteraction.INTERACTIONS;
+
+ Objects.requireNonNull(map);
+@@ -305,6 +380,8 @@
+ LAVA = newInteractionMap("lava");
+ POWDER_SNOW = newInteractionMap("powder_snow");
+ }
++ */
++ // CraftBukkit end
+
+ public static record InteractionMap(String name, Map- map) {
+
diff --git a/paper-server/patches/sources/net/minecraft/core/component/DataComponentPatch.java.patch b/paper-server/patches/sources/net/minecraft/core/component/DataComponentPatch.java.patch
new file mode 100644
index 0000000000..53846b237e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/component/DataComponentPatch.java.patch
@@ -0,0 +1,70 @@
+--- a/net/minecraft/core/component/DataComponentPatch.java
++++ b/net/minecraft/core/component/DataComponentPatch.java
+@@ -61,7 +61,7 @@
+ }
+ }
+
+- return reference2objectmap;
++ return (Reference2ObjectMap) reference2objectmap; // CraftBukkit - decompile error
+ });
+ public static final StreamCodec
STREAM_CODEC = new StreamCodec() {
+ public DataComponentPatch decode(RegistryFriendlyByteBuf registryfriendlybytebuf) {
+@@ -144,7 +144,13 @@
+ }
+
+ private static void encodeComponent(RegistryFriendlyByteBuf buf, DataComponentType type, Object value) {
+- type.streamCodec().encode(buf, value);
++ // Paper start - codec errors of random anonymous classes are useless
++ try {
++ type.streamCodec().encode(buf, (T) value); // CraftBukkit - decompile error
++ } catch (final Exception e) {
++ throw new RuntimeException("Error encoding component " + type, e);
++ }
++ // Paper end - codec errors of random anonymous classes are useless
+ }
+ };
+ private static final String REMOVED_PREFIX = "!";
+@@ -270,7 +276,43 @@
+ private final Reference2ObjectMap, Optional>> map = new Reference2ObjectArrayMap();
+
+ Builder() {}
++
++ // CraftBukkit start
++ public void copy(DataComponentPatch orig) {
++ this.map.putAll(orig.map);
++ }
++
++ public void clear(DataComponentType> type) {
++ this.map.remove(type);
++ }
++
++ public boolean isSet(DataComponentType> type) {
++ return this.map.containsKey(type);
++ }
++
++ public boolean isEmpty() {
++ return this.map.isEmpty();
++ }
+
++ @Override
++ public boolean equals(Object object) {
++ if (this == object) {
++ return true;
++ }
++
++ if (object instanceof DataComponentPatch.Builder patch) {
++ return this.map.equals(patch.map);
++ }
++
++ return false;
++ }
++
++ @Override
++ public int hashCode() {
++ return this.map.hashCode();
++ }
++ // CraftBukkit end
++
+ public DataComponentPatch.Builder set(DataComponentType type, T value) {
+ this.map.put(type, Optional.of(value));
+ return this;
diff --git a/paper-server/patches/sources/net/minecraft/core/component/DataComponents.java.patch b/paper-server/patches/sources/net/minecraft/core/component/DataComponents.java.patch
new file mode 100644
index 0000000000..eb470da08e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/component/DataComponents.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/core/component/DataComponents.java
++++ b/net/minecraft/core/component/DataComponents.java
+@@ -180,10 +180,10 @@
+ "map_post_processing", builder -> builder.networkSynchronized(MapPostProcessing.STREAM_CODEC)
+ );
+ public static final DataComponentType CHARGED_PROJECTILES = register(
+- "charged_projectiles", builder -> builder.persistent(ChargedProjectiles.CODEC).networkSynchronized(ChargedProjectiles.STREAM_CODEC).cacheEncoding()
++ "charged_projectiles", builder -> builder.persistent(ChargedProjectiles.CODEC).networkSynchronized(io.papermc.paper.util.DataSanitizationUtil.CHARGED_PROJECTILES).cacheEncoding() // Paper - sanitize charged projectiles
+ );
+ public static final DataComponentType BUNDLE_CONTENTS = register(
+- "bundle_contents", builder -> builder.persistent(BundleContents.CODEC).networkSynchronized(BundleContents.STREAM_CODEC).cacheEncoding()
++ "bundle_contents", builder -> builder.persistent(BundleContents.CODEC).networkSynchronized(io.papermc.paper.util.DataSanitizationUtil.BUNDLE_CONTENTS).cacheEncoding() // Paper - sanitize bundle contents
+ );
+ public static final DataComponentType POTION_CONTENTS = register(
+ "potion_contents", builder -> builder.persistent(PotionContents.CODEC).networkSynchronized(PotionContents.STREAM_CODEC).cacheEncoding()
+@@ -250,7 +250,7 @@
+ "pot_decorations", builder -> builder.persistent(PotDecorations.CODEC).networkSynchronized(PotDecorations.STREAM_CODEC).cacheEncoding()
+ );
+ public static final DataComponentType CONTAINER = register(
+- "container", builder -> builder.persistent(ItemContainerContents.CODEC).networkSynchronized(ItemContainerContents.STREAM_CODEC).cacheEncoding()
++ "container", builder -> builder.persistent(ItemContainerContents.CODEC).networkSynchronized(io.papermc.paper.util.DataSanitizationUtil.CONTAINER).cacheEncoding() // Paper - sanitize container contents
+ );
+ public static final DataComponentType BLOCK_STATE = register(
+ "block_state", builder -> builder.persistent(BlockItemStateProperties.CODEC).networkSynchronized(BlockItemStateProperties.STREAM_CODEC).cacheEncoding()
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch
new file mode 100644
index 0000000000..a97acf2799
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch
@@ -0,0 +1,58 @@
+--- a/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
++++ b/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
+@@ -11,6 +11,11 @@
+ import net.minecraft.world.item.ItemStack;
+ import net.minecraft.world.level.block.DispenserBlock;
+ import net.minecraft.world.phys.Vec3;
++// CraftBukkit start
++import org.bukkit.craftbukkit.block.CraftBlock;
++import org.bukkit.craftbukkit.inventory.CraftItemStack;
++import org.bukkit.event.block.BlockDispenseEvent;
++// CraftBukkit end
+
+ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior {
+
+@@ -43,14 +48,40 @@
+ d4 = 0.0D;
+ }
+
++ // CraftBukkit start
++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink at end and single item in event
++ org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
++
++ BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d1, d2 + d4, d3));
++ if (!DispenserBlock.eventFired) {
++ worldserver.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ // stack.grow(1); // Paper - shrink below
++ return stack;
++ }
++
++ boolean shrink = true; // Paper
++ if (!event.getItem().equals(craftItem)) {
++ shrink = false; // Paper - shrink below
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++ // CraftBukkit end
+ AbstractBoat abstractboat = (AbstractBoat) this.type.create(worldserver, EntitySpawnReason.DISPENSER);
+
+ if (abstractboat != null) {
+- abstractboat.setInitialPos(d1, d2 + d4, d3);
++ abstractboat.setInitialPos(event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ()); // CraftBukkit
+ EntityType.createDefaultStackConfig(worldserver, stack, (Player) null).accept(abstractboat);
+ abstractboat.setYRot(enumdirection.toYRot());
+- worldserver.addFreshEntity(abstractboat);
+- stack.shrink(1);
++ if (worldserver.addFreshEntity(abstractboat) && shrink) stack.shrink(1); // Paper - if entity add was successful and supposed to shrink
+ }
+
+ return stack;
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch
new file mode 100644
index 0000000000..9a3359fc85
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch
@@ -0,0 +1,126 @@
+--- a/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java
++++ b/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java
+@@ -6,47 +6,114 @@
+ import net.minecraft.world.item.ItemStack;
+ import net.minecraft.world.level.Level;
+ import net.minecraft.world.level.block.DispenserBlock;
++// CraftBukkit start
++import org.bukkit.craftbukkit.block.CraftBlock;
++import org.bukkit.craftbukkit.inventory.CraftItemStack;
++import org.bukkit.craftbukkit.util.CraftVector;
++import org.bukkit.event.block.BlockDispenseEvent;
++// CraftBukkit end
+
+ public class DefaultDispenseItemBehavior implements DispenseItemBehavior {
++ private Direction enumdirection; // Paper - cache facing direction
+
+ private static final int DEFAULT_ACCURACY = 6;
+
++ // CraftBukkit start
++ private boolean dropper;
++
++ public DefaultDispenseItemBehavior(boolean dropper) {
++ this.dropper = dropper;
++ }
++ // CraftBukkit end
++
+ public DefaultDispenseItemBehavior() {}
+
+ @Override
+ public final ItemStack dispense(BlockSource pointer, ItemStack stack) {
++ enumdirection = pointer.state().getValue(DispenserBlock.FACING); // Paper - cache facing direction
+ ItemStack itemstack1 = this.execute(pointer, stack);
+
+ this.playSound(pointer);
+- this.playAnimation(pointer, (Direction) pointer.state().getValue(DispenserBlock.FACING));
++ this.playAnimation(pointer, enumdirection); // Paper - cache facing direction
+ return itemstack1;
+ }
+
+ protected ItemStack execute(BlockSource pointer, ItemStack stack) {
+- Direction enumdirection = (Direction) pointer.state().getValue(DispenserBlock.FACING);
++ // Paper - cached enum direction
+ Position iposition = DispenserBlock.getDispensePosition(pointer);
+ ItemStack itemstack1 = stack.split(1);
+
+- DefaultDispenseItemBehavior.spawnItem(pointer.level(), itemstack1, 6, enumdirection, iposition);
++ // CraftBukkit start
++ if (!DefaultDispenseItemBehavior.spawnItem(pointer.level(), itemstack1, 6, enumdirection, pointer, this.dropper)) {
++ stack.grow(1);
++ }
++ // CraftBukkit end
+ return stack;
+ }
+
+ public static void spawnItem(Level world, ItemStack stack, int speed, Direction side, Position pos) {
+- double d0 = pos.x();
+- double d1 = pos.y();
+- double d2 = pos.z();
++ // CraftBukkit start
++ ItemEntity entityitem = DefaultDispenseItemBehavior.prepareItem(world, stack, speed, side, pos);
++ world.addFreshEntity(entityitem);
++ }
+
+- if (side.getAxis() == Direction.Axis.Y) {
++ private static ItemEntity prepareItem(Level world, ItemStack itemstack, int i, Direction enumdirection, Position iposition) {
++ // CraftBukkit end
++ double d0 = iposition.x();
++ double d1 = iposition.y();
++ double d2 = iposition.z();
++
++ if (enumdirection.getAxis() == Direction.Axis.Y) {
+ d1 -= 0.125D;
+ } else {
+ d1 -= 0.15625D;
+ }
+
+- ItemEntity entityitem = new ItemEntity(world, d0, d1, d2, stack);
++ ItemEntity entityitem = new ItemEntity(world, d0, d1, d2, itemstack);
+ double d3 = world.random.nextDouble() * 0.1D + 0.2D;
+
+- entityitem.setDeltaMovement(world.random.triangle((double) side.getStepX() * d3, 0.0172275D * (double) speed), world.random.triangle(0.2D, 0.0172275D * (double) speed), world.random.triangle((double) side.getStepZ() * d3, 0.0172275D * (double) speed));
++ entityitem.setDeltaMovement(world.random.triangle((double) enumdirection.getStepX() * d3, 0.0172275D * (double) i), world.random.triangle(0.2D, 0.0172275D * (double) i), world.random.triangle((double) enumdirection.getStepZ() * d3, 0.0172275D * (double) i));
++ // CraftBukkit start
++ return entityitem;
++ }
++
++ // CraftBukkit - void -> boolean return, IPosition -> ISourceBlock last argument, dropper
++ public static boolean spawnItem(Level world, ItemStack itemstack, int i, Direction enumdirection, BlockSource sourceblock, boolean dropper) {
++ if (itemstack.isEmpty()) return true;
++ Position iposition = DispenserBlock.getDispensePosition(sourceblock);
++ ItemEntity entityitem = DefaultDispenseItemBehavior.prepareItem(world, itemstack, i, enumdirection, iposition);
++
++ org.bukkit.block.Block block = CraftBlock.at(world, sourceblock.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack);
++
++ BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), CraftVector.toBukkit(entityitem.getDeltaMovement()));
++ if (!DispenserBlock.eventFired) {
++ world.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ return false;
++ }
++
++ entityitem.setItem(CraftItemStack.asNMSCopy(event.getItem()));
++ entityitem.setDeltaMovement(CraftVector.toNMS(event.getVelocity()));
++
++ if (!dropper && !event.getItem().getType().equals(craftItem.getType())) {
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(sourceblock, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior.getClass() != DefaultDispenseItemBehavior.class) {
++ idispensebehavior.dispense(sourceblock, eventStack);
++ } else {
++ world.addFreshEntity(entityitem);
++ }
++ return false;
++ }
++
+ world.addFreshEntity(entityitem);
++
++ return true;
++ // CraftBukkit end
+ }
+
+ protected void playSound(BlockSource pointer) {
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch
new file mode 100644
index 0000000000..824b11e0d1
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch
@@ -0,0 +1,651 @@
+--- a/net/minecraft/core/dispenser/DispenseItemBehavior.java
++++ b/net/minecraft/core/dispenser/DispenseItemBehavior.java
+@@ -28,6 +28,7 @@
+ import net.minecraft.world.entity.item.PrimedTnt;
+ import net.minecraft.world.entity.player.Player;
+ import net.minecraft.world.item.BoneMealItem;
++import net.minecraft.world.item.BucketItem;
+ import net.minecraft.world.item.DispensibleContainerItem;
+ import net.minecraft.world.item.DyeColor;
+ import net.minecraft.world.item.HoneycombItem;
+@@ -47,7 +48,9 @@
+ import net.minecraft.world.level.block.CandleCakeBlock;
+ import net.minecraft.world.level.block.CarvedPumpkinBlock;
+ import net.minecraft.world.level.block.DispenserBlock;
++import net.minecraft.world.level.block.LiquidBlockContainer;
+ import net.minecraft.world.level.block.RespawnAnchorBlock;
++import net.minecraft.world.level.block.SaplingBlock;
+ import net.minecraft.world.level.block.ShulkerBoxBlock;
+ import net.minecraft.world.level.block.SkullBlock;
+ import net.minecraft.world.level.block.TntBlock;
+@@ -62,6 +65,17 @@
+ import net.minecraft.world.phys.AABB;
+ import net.minecraft.world.phys.BlockHitResult;
+ import org.slf4j.Logger;
++import org.bukkit.Location;
++import org.bukkit.TreeType;
++import org.bukkit.craftbukkit.block.CraftBlock;
++import org.bukkit.craftbukkit.inventory.CraftItemStack;
++import org.bukkit.craftbukkit.util.CraftLocation;
++import org.bukkit.craftbukkit.util.DummyGeneratorAccess;
++import org.bukkit.event.block.BlockDispenseArmorEvent;
++import org.bukkit.event.block.BlockDispenseEvent;
++import org.bukkit.event.block.BlockFertilizeEvent;
++import org.bukkit.event.world.StructureGrowEvent;
++// CraftBukkit end
+
+ public interface DispenseItemBehavior {
+
+@@ -90,14 +104,47 @@
+ Direction enumdirection = (Direction) pointer.state().getValue(DispenserBlock.FACING);
+ EntityType> entitytypes = ((SpawnEggItem) stack.getItem()).getType(pointer.level().registryAccess(), stack);
+
++ // CraftBukkit start
++ ServerLevel worldserver = pointer.level();
++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event
++ org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
++
++ BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
++ if (!DispenserBlock.eventFired) {
++ worldserver.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ // stack.grow(1); // Paper - shrink below
++ return stack;
++ }
++
++ boolean shrink = true; // Paper
++ if (!event.getItem().equals(craftItem)) {
++ shrink = false; // Paper - shrink below
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ // Paper start - track changed items in the dispense event
++ itemstack1 = CraftItemStack.unwrap(event.getItem()); // unwrap is safe because the stack won't be modified
++ entitytypes = ((SpawnEggItem) itemstack1.getItem()).getType(worldserver.registryAccess(), itemstack1);
++ // Paper end - track changed item from dispense event
++ }
++
+ try {
+- entitytypes.spawn(pointer.level(), stack, (Player) null, pointer.pos().relative(enumdirection), EntitySpawnReason.DISPENSER, enumdirection != Direction.UP, false);
++ entitytypes.spawn(pointer.level(), itemstack1, (Player) null, pointer.pos().relative(enumdirection), EntitySpawnReason.DISPENSER, enumdirection != Direction.UP, false); // Paper - track changed item in dispense event
+ } catch (Exception exception) {
+- null.LOGGER.error("Error while dispensing spawn egg from dispenser at {}", pointer.pos(), exception);
++ DispenseItemBehavior.LOGGER.error("Error while dispensing spawn egg from dispenser at {}", pointer.pos(), exception); // CraftBukkit - decompile error
+ return ItemStack.EMPTY;
+ }
+
+- stack.shrink(1);
++ if (shrink) stack.shrink(1); // Paper - actually handle here
++ // CraftBukkit end
+ pointer.level().gameEvent((Entity) null, (Holder) GameEvent.ENTITY_PLACE, pointer.pos());
+ return stack;
+ }
+@@ -116,13 +163,43 @@
+ Direction enumdirection = (Direction) pointer.state().getValue(DispenserBlock.FACING);
+ BlockPos blockposition = pointer.pos().relative(enumdirection);
+ ServerLevel worldserver = pointer.level();
++
++ // CraftBukkit start
++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event
++ org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
++
++ BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
++ if (!DispenserBlock.eventFired) {
++ worldserver.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ // stack.grow(1); // Paper - shrink below
++ return stack;
++ }
++
++ boolean shrink = true; // Paper
++ if (!event.getItem().equals(craftItem)) {
++ shrink = false; // Paper - shrink below
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++ // CraftBukkit end
++
++ final ItemStack newStack = CraftItemStack.unwrap(event.getItem()); // Paper - use event itemstack (unwrap is fine here because the stack won't be modified)
+ Consumer consumer = EntityType.appendDefaultStackConfig((entityarmorstand) -> {
+ entityarmorstand.setYRot(enumdirection.toYRot());
+- }, worldserver, stack, (Player) null);
++ }, worldserver, newStack, (Player) null); // Paper - track changed items in the dispense event
+ ArmorStand entityarmorstand = (ArmorStand) EntityType.ARMOR_STAND.spawn(worldserver, consumer, blockposition, EntitySpawnReason.DISPENSER, false, false);
+
+ if (entityarmorstand != null) {
+- stack.shrink(1);
++ if (shrink) stack.shrink(1); // Paper - actually handle here
+ }
+
+ return stack;
+@@ -141,7 +218,36 @@
+ });
+
+ if (!list.isEmpty()) {
+- ((Saddleable) list.get(0)).equipSaddle(stack.split(1), SoundSource.BLOCKS);
++ // CraftBukkit start
++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event
++ ServerLevel world = pointer.level();
++ org.bukkit.block.Block block = CraftBlock.at(world, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
++
++ BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) list.get(0).getBukkitEntity());
++ if (!DispenserBlock.eventFired) {
++ world.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ // stack.grow(1); // Paper - shrink below
++ return stack;
++ }
++
++ boolean shrink = true; // Paper
++ if (!event.getItem().equals(craftItem)) {
++ shrink = false; // Paper - shrink below
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) { // Paper - fix possible StackOverflowError
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++ ((Saddleable) list.get(0)).equipSaddle(CraftItemStack.asNMSCopy(event.getItem()), SoundSource.BLOCKS); // Paper - track changed items in dispense event
++ // CraftBukkit end
++ if (shrink) stack.shrink(1); // Paper - actually handle here
+ this.setSuccess(true);
+ return stack;
+ } else {
+@@ -166,9 +272,38 @@
+ }
+
+ entityhorsechestedabstract = (AbstractChestedHorse) iterator1.next();
+- } while (!entityhorsechestedabstract.isTamed() || !entityhorsechestedabstract.getSlot(499).set(stack));
++ // CraftBukkit start
++ } while (!entityhorsechestedabstract.isTamed());
++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below
++ ServerLevel world = pointer.level();
++ org.bukkit.block.Block block = CraftBlock.at(world, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
+
+- stack.shrink(1);
++ BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityhorsechestedabstract.getBukkitEntity());
++ if (!DispenserBlock.eventFired) {
++ world.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ // stack.grow(1); // Paper - shrink below (this was actually missing and should be here, added it commented out to be consistent)
++ return stack;
++ }
++
++ boolean shrink = true; // Paper
++ if (!event.getItem().equals(craftItem)) {
++ shrink = false; // Paper - shrink below
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) { // Paper - fix possible StackOverflowError
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++ entityhorsechestedabstract.getSlot(499).set(CraftItemStack.asNMSCopy(event.getItem()));
++ // CraftBukkit end
++
++ if (shrink) stack.shrink(1); // Paper - actually handle here
+ this.setSuccess(true);
+ return stack;
+ }
+@@ -202,8 +337,50 @@
+ BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING));
+ ServerLevel worldserver = pointer.level();
+
++ // CraftBukkit start
++ int x = blockposition.getX();
++ int y = blockposition.getY();
++ int z = blockposition.getZ();
++ BlockState iblockdata = worldserver.getBlockState(blockposition);
++ ItemStack dispensedItem = stack; // Paper - track changed item from the dispense event
++ // Paper start - correctly check if the bucket place will succeed
++ /* Taken from SolidBucketItem#emptyContents */
++ boolean willEmptyContentsSolidBucketItem = dispensiblecontaineritem instanceof net.minecraft.world.item.SolidBucketItem && worldserver.isInWorldBounds(blockposition) && iblockdata.isAir();
++ /* Taken from BucketItem#emptyContents */
++ boolean willEmptyBucketItem = dispensiblecontaineritem instanceof final BucketItem bucketItem && bucketItem.content instanceof net.minecraft.world.level.material.FlowingFluid && (iblockdata.isAir() || iblockdata.canBeReplaced(bucketItem.content) || (iblockdata.getBlock() instanceof LiquidBlockContainer liquidBlockContainer && liquidBlockContainer.canPlaceLiquid(null, worldserver, blockposition, iblockdata, bucketItem.content)));
++ if (willEmptyContentsSolidBucketItem || willEmptyBucketItem) {
++ // Paper end - correctly check if the bucket place will succeed
++ org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
++
++ BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(x, y, z));
++ if (!DispenserBlock.eventFired) {
++ worldserver.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ return stack;
++ }
++
++ if (!event.getItem().equals(craftItem)) {
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++
++ // Paper start - track changed item from dispense event
++ dispensedItem = CraftItemStack.unwrap(event.getItem()); // unwrap is safe here as the stack isn't mutated
++ dispensiblecontaineritem = (DispensibleContainerItem) dispensedItem.getItem();
++ // Paper end - track changed item from dispense event
++ }
++ // CraftBukkit end
++
+ if (dispensiblecontaineritem.emptyContents((Player) null, worldserver, blockposition, (BlockHitResult) null)) {
+- dispensiblecontaineritem.checkExtraContent((Player) null, worldserver, stack, blockposition);
++ dispensiblecontaineritem.checkExtraContent((Player) null, worldserver, dispensedItem, blockposition); // Paper - track changed item from dispense event
+ return this.consumeWithRemainder(pointer, stack, new ItemStack(Items.BUCKET));
+ } else {
+ return this.defaultDispenseItemBehavior.dispense(pointer, stack);
+@@ -229,7 +406,7 @@
+ Block block = iblockdata.getBlock();
+
+ if (block instanceof BucketPickup ifluidsource) {
+- ItemStack itemstack1 = ifluidsource.pickupBlock((Player) null, worldserver, blockposition, iblockdata);
++ ItemStack itemstack1 = ifluidsource.pickupBlock((Player) null, DummyGeneratorAccess.INSTANCE, blockposition, iblockdata); // CraftBukkit
+
+ if (itemstack1.isEmpty()) {
+ return super.execute(pointer, stack);
+@@ -237,6 +414,32 @@
+ worldserver.gameEvent((Entity) null, (Holder) GameEvent.FLUID_PICKUP, blockposition);
+ Item item = itemstack1.getItem();
+
++ // CraftBukkit start
++ org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
++
++ BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ()));
++ if (!DispenserBlock.eventFired) {
++ worldserver.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ return stack;
++ }
++
++ if (!event.getItem().equals(craftItem)) {
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++
++ itemstack1 = ifluidsource.pickupBlock((Player) null, worldserver, blockposition, iblockdata); // From above
++ // CraftBukkit end
++
+ return this.consumeWithRemainder(pointer, stack, new ItemStack(item));
+ }
+ } else {
+@@ -249,16 +452,44 @@
+ protected ItemStack execute(BlockSource pointer, ItemStack stack) {
+ ServerLevel worldserver = pointer.level();
+
++ // CraftBukkit start
++ org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items
++
++ BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
++ if (!DispenserBlock.eventFired) {
++ worldserver.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ return stack;
++ }
++
++ if (!event.getItem().equals(craftItem)) {
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++ // CraftBukkit end
++
+ this.setSuccess(true);
+ Direction enumdirection = (Direction) pointer.state().getValue(DispenserBlock.FACING);
+ BlockPos blockposition = pointer.pos().relative(enumdirection);
+ BlockState iblockdata = worldserver.getBlockState(blockposition);
+
+ if (BaseFireBlock.canBePlacedAt(worldserver, blockposition, enumdirection)) {
+- worldserver.setBlockAndUpdate(blockposition, BaseFireBlock.getState(worldserver, blockposition));
+- worldserver.gameEvent((Entity) null, (Holder) GameEvent.BLOCK_PLACE, blockposition);
++ // CraftBukkit start - Ignition by dispensing flint and steel
++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(worldserver, blockposition, pointer.pos()).isCancelled()) {
++ worldserver.setBlockAndUpdate(blockposition, BaseFireBlock.getState(worldserver, blockposition));
++ worldserver.gameEvent((Entity) null, (Holder) GameEvent.BLOCK_PLACE, blockposition);
++ }
++ // CraftBukkit end
+ } else if (!CampfireBlock.canLight(iblockdata) && !CandleBlock.canLight(iblockdata) && !CandleCakeBlock.canLight(iblockdata)) {
+- if (iblockdata.getBlock() instanceof TntBlock) {
++ if (iblockdata.getBlock() instanceof TntBlock && org.bukkit.craftbukkit.event.CraftEventFactory.callTNTPrimeEvent(worldserver, blockposition, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.DISPENSER, null, pointer.pos())) { // CraftBukkit - TNTPrimeEvent
+ TntBlock.explode(worldserver, blockposition);
+ worldserver.removeBlock(blockposition, false);
+ } else {
+@@ -283,13 +514,64 @@
+ this.setSuccess(true);
+ ServerLevel worldserver = pointer.level();
+ BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING));
++ // CraftBukkit start
++ org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
+
++ BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
++ if (!DispenserBlock.eventFired) {
++ worldserver.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ return stack;
++ }
++
++ if (!event.getItem().equals(craftItem)) {
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++
++ worldserver.captureTreeGeneration = true;
++ // CraftBukkit end
++
+ if (!BoneMealItem.growCrop(stack, worldserver, blockposition) && !BoneMealItem.growWaterPlant(stack, worldserver, blockposition, (Direction) null)) {
+ this.setSuccess(false);
+ } else if (!worldserver.isClientSide) {
+ worldserver.levelEvent(1505, blockposition, 15);
+ }
++ // CraftBukkit start
++ worldserver.captureTreeGeneration = false;
++ if (worldserver.capturedBlockStates.size() > 0) {
++ TreeType treeType = SaplingBlock.treeType;
++ SaplingBlock.treeType = null;
++ Location location = CraftLocation.toBukkit(blockposition, worldserver.getWorld());
++ List blocks = new java.util.ArrayList<>(worldserver.capturedBlockStates.values());
++ worldserver.capturedBlockStates.clear();
++ StructureGrowEvent structureEvent = null;
++ if (treeType != null) {
++ structureEvent = new StructureGrowEvent(location, treeType, false, null, blocks);
++ org.bukkit.Bukkit.getPluginManager().callEvent(structureEvent);
++ }
+
++ BlockFertilizeEvent fertilizeEvent = new BlockFertilizeEvent(location.getBlock(), null, blocks);
++ fertilizeEvent.setCancelled(structureEvent != null && structureEvent.isCancelled());
++ org.bukkit.Bukkit.getPluginManager().callEvent(fertilizeEvent);
++
++ if (!fertilizeEvent.isCancelled()) {
++ for (org.bukkit.block.BlockState blockstate : blocks) {
++ blockstate.update(true);
++ worldserver.checkCapturedTreeStateForObserverNotify(blockposition, (org.bukkit.craftbukkit.block.CraftBlockState) blockstate); // Paper - notify observers even if grow failed
++ }
++ }
++ }
++ // CraftBukkit end
++
+ return stack;
+ }
+ });
+@@ -298,12 +580,42 @@
+ protected ItemStack execute(BlockSource pointer, ItemStack stack) {
+ ServerLevel worldserver = pointer.level();
+ BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING));
+- PrimedTnt entitytntprimed = new PrimedTnt(worldserver, (double) blockposition.getX() + 0.5D, (double) blockposition.getY(), (double) blockposition.getZ() + 0.5D, (LivingEntity) null);
++ // CraftBukkit start
++ // EntityTNTPrimed entitytntprimed = new EntityTNTPrimed(worldserver, (double) blockposition.getX() + 0.5D, (double) blockposition.getY(), (double) blockposition.getZ() + 0.5D, (EntityLiving) null);
+
++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink at end and single item in event
++ org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
++
++ BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) blockposition.getX() + 0.5D, (double) blockposition.getY(), (double) blockposition.getZ() + 0.5D));
++ if (!DispenserBlock.eventFired) {
++ worldserver.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ // stack.grow(1); // Paper - shrink below
++ return stack;
++ }
++
++ boolean shrink = true; // Paper
++ if (!event.getItem().equals(craftItem)) {
++ shrink = false; // Paper - shrink below
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++
++ PrimedTnt entitytntprimed = new PrimedTnt(worldserver, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), (LivingEntity) null);
++ // CraftBukkit end
++
+ worldserver.addFreshEntity(entitytntprimed);
+ worldserver.playSound((Player) null, entitytntprimed.getX(), entitytntprimed.getY(), entitytntprimed.getZ(), SoundEvents.TNT_PRIMED, SoundSource.BLOCKS, 1.0F, 1.0F);
+ worldserver.gameEvent((Entity) null, (Holder) GameEvent.ENTITY_PLACE, blockposition);
+- stack.shrink(1);
++ if (shrink) stack.shrink(1); // Paper - actually handle here
+ return stack;
+ }
+ });
+@@ -313,7 +625,31 @@
+ ServerLevel worldserver = pointer.level();
+ Direction enumdirection = (Direction) pointer.state().getValue(DispenserBlock.FACING);
+ BlockPos blockposition = pointer.pos().relative(enumdirection);
++
++ // CraftBukkit start
++ org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
++
++ BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ()));
++ if (!DispenserBlock.eventFired) {
++ worldserver.getCraftServer().getPluginManager().callEvent(event);
++ }
+
++ if (event.isCancelled()) {
++ return stack;
++ }
++
++ if (!event.getItem().equals(craftItem)) {
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++ // CraftBukkit end
++
+ if (worldserver.isEmptyBlock(blockposition) && WitherSkullBlock.canSpawnMob(worldserver, blockposition, stack)) {
+ worldserver.setBlock(blockposition, (BlockState) Blocks.WITHER_SKELETON_SKULL.defaultBlockState().setValue(SkullBlock.ROTATION, RotationSegment.convertToSegment(enumdirection)), 3);
+ worldserver.gameEvent((Entity) null, (Holder) GameEvent.BLOCK_PLACE, blockposition);
+@@ -326,7 +662,7 @@
+ stack.shrink(1);
+ this.setSuccess(true);
+ } else {
+- this.setSuccess(EquipmentDispenseItemBehavior.dispenseEquipment(pointer, stack));
++ this.setSuccess(EquipmentDispenseItemBehavior.dispenseEquipment(pointer, stack, this)); // Paper - fix possible StackOverflowError
+ }
+
+ return stack;
+@@ -339,6 +675,30 @@
+ BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING));
+ CarvedPumpkinBlock blockpumpkincarved = (CarvedPumpkinBlock) Blocks.CARVED_PUMPKIN;
+
++ // CraftBukkit start
++ org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
++
++ BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ()));
++ if (!DispenserBlock.eventFired) {
++ worldserver.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ return stack;
++ }
++
++ if (!event.getItem().equals(craftItem)) {
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++ // CraftBukkit end
++
+ if (worldserver.isEmptyBlock(blockposition) && blockpumpkincarved.canSpawnGolem(worldserver, blockposition)) {
+ if (!worldserver.isClientSide) {
+ worldserver.setBlock(blockposition, blockpumpkincarved.defaultBlockState(), 3);
+@@ -348,7 +708,7 @@
+ stack.shrink(1);
+ this.setSuccess(true);
+ } else {
+- this.setSuccess(EquipmentDispenseItemBehavior.dispenseEquipment(pointer, stack));
++ this.setSuccess(EquipmentDispenseItemBehavior.dispenseEquipment(pointer, stack, this)); // Paper - fix possible StackOverflowError
+ }
+
+ return stack;
+@@ -377,6 +737,30 @@
+ BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING));
+ BlockState iblockdata = worldserver.getBlockState(blockposition);
+
++ // CraftBukkit start
++ org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - only single item in event
++
++ BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ()));
++ if (!DispenserBlock.eventFired) {
++ worldserver.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ return stack;
++ }
++
++ if (!event.getItem().equals(craftItem)) {
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++ // CraftBukkit end
++
+ if (iblockdata.is(BlockTags.BEEHIVES, (blockbase_blockdata) -> {
+ return blockbase_blockdata.hasProperty(BeehiveBlock.HONEY_LEVEL) && blockbase_blockdata.getBlock() instanceof BeehiveBlock;
+ }) && (Integer) iblockdata.getValue(BeehiveBlock.HONEY_LEVEL) >= 5) {
+@@ -402,6 +786,13 @@
+ this.setSuccess(true);
+ if (iblockdata.is(Blocks.RESPAWN_ANCHOR)) {
+ if ((Integer) iblockdata.getValue(RespawnAnchorBlock.CHARGE) != 4) {
++ // Paper start - Call missing BlockDispenseEvent
++ ItemStack result = org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDispenseEvent(pointer, blockposition, stack, this);
++ if (result != null) {
++ this.setSuccess(false);
++ return result;
++ }
++ // Paper end - Call missing BlockDispenseEvent
+ RespawnAnchorBlock.charge((Entity) null, worldserver, blockposition, iblockdata);
+ stack.shrink(1);
+ } else {
+@@ -426,6 +817,31 @@
+ this.setSuccess(false);
+ return stack;
+ } else {
++ // CraftBukkit start
++ ItemStack itemstack1 = stack;
++ ServerLevel world = pointer.level();
++ org.bukkit.block.Block block = CraftBlock.at(world, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); // Paper - ignore stack size on damageable items
++
++ BlockDispenseEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) list.get(0).getBukkitEntity());
++ if (!DispenserBlock.eventFired) {
++ world.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ return stack;
++ }
++
++ if (!event.getItem().equals(craftItem)) {
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) { // Paper - fix possible StackOverflowError
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++ // CraftBukkit end
+ Iterator iterator1 = list.iterator();
+
+ Armadillo armadillo;
+@@ -454,6 +870,13 @@
+ Optional optional = HoneycombItem.getWaxed(iblockdata);
+
+ if (optional.isPresent()) {
++ // Paper start - Call missing BlockDispenseEvent
++ ItemStack result = org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDispenseEvent(pointer, blockposition, stack, this);
++ if (result != null) {
++ this.setSuccess(false);
++ return result;
++ }
++ // Paper end - Call missing BlockDispenseEvent
+ worldserver.setBlockAndUpdate(blockposition, (BlockState) optional.get());
+ worldserver.levelEvent(3003, blockposition, 0);
+ stack.shrink(1);
+@@ -481,6 +904,12 @@
+ if (!worldserver.getBlockState(blockposition1).is(BlockTags.CONVERTABLE_TO_MUD)) {
+ return this.defaultDispenseItemBehavior.dispense(pointer, stack);
+ } else {
++ // Paper start - Call missing BlockDispenseEvent
++ ItemStack result = org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDispenseEvent(pointer, blockposition1, stack, this);
++ if (result != null) {
++ return result;
++ }
++ // Paper end - Call missing BlockDispenseEvent
+ if (!worldserver.isClientSide) {
+ for (int k = 0; k < 5; ++k) {
+ worldserver.sendParticles(ParticleTypes.SPLASH, (double) blockposition.getX() + worldserver.random.nextDouble(), (double) (blockposition.getY() + 1), (double) blockposition.getZ() + worldserver.random.nextDouble(), 1, 0.0D, 0.0D, 0.0D, 1.0D);
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch
new file mode 100644
index 0000000000..18eb499fba
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch
@@ -0,0 +1,82 @@
+--- a/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java
++++ b/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java
+@@ -7,8 +7,13 @@
+ import net.minecraft.world.entity.LivingEntity;
+ import net.minecraft.world.entity.Mob;
+ import net.minecraft.world.item.ItemStack;
++import net.minecraft.world.level.Level;
+ import net.minecraft.world.level.block.DispenserBlock;
+ import net.minecraft.world.phys.AABB;
++import org.bukkit.craftbukkit.block.CraftBlock;
++import org.bukkit.craftbukkit.inventory.CraftItemStack;
++import org.bukkit.event.block.BlockDispenseArmorEvent;
++// CraftBukkit end
+
+ public class EquipmentDispenseItemBehavior extends DefaultDispenseItemBehavior {
+
+@@ -18,10 +23,15 @@
+
+ @Override
+ protected ItemStack execute(BlockSource pointer, ItemStack stack) {
+- return EquipmentDispenseItemBehavior.dispenseEquipment(pointer, stack) ? stack : super.execute(pointer, stack);
++ return EquipmentDispenseItemBehavior.dispenseEquipment(pointer, stack, this) ? stack : super.execute(pointer, stack); // Paper - fix possible StackOverflowError
+ }
+
+- public static boolean dispenseEquipment(BlockSource pointer, ItemStack stack) {
++ @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper
++ public static boolean dispenseEquipment(BlockSource pointer, ItemStack armor) {
++ // Paper start
++ return dispenseEquipment(pointer, armor, null);
++ }
++ public static boolean dispenseEquipment(BlockSource pointer, ItemStack stack, @javax.annotation.Nullable DispenseItemBehavior currentBehavior) {
+ BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING));
+ List list = pointer.level().getEntitiesOfClass(LivingEntity.class, new AABB(blockposition), (entityliving) -> {
+ return entityliving.canEquipWithDispenser(stack);
+@@ -32,9 +42,37 @@
+ } else {
+ LivingEntity entityliving = (LivingEntity) list.getFirst();
+ EquipmentSlot enumitemslot = entityliving.getEquipmentSlotForItem(stack);
+- ItemStack itemstack1 = stack.split(1);
++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event
+
+- entityliving.setItemSlot(enumitemslot, itemstack1);
++ // CraftBukkit start
++ Level world = pointer.level();
++ org.bukkit.block.Block block = CraftBlock.at(world, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
++
++ BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityliving.getBukkitEntity());
++ if (!DispenserBlock.eventFired) {
++ world.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ // stack.grow(1); // Paper - shrink below
++ return false;
++ }
++
++ boolean shrink = true; // Paper
++ if (!event.getItem().equals(craftItem)) {
++ shrink = false; // Paper - shrink below
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && (currentBehavior == null || idispensebehavior != currentBehavior)) { // Paper - fix possible StackOverflowError
++ idispensebehavior.dispense(pointer, eventStack);
++ return true;
++ }
++ }
++
++ entityliving.setItemSlot(enumitemslot, CraftItemStack.asNMSCopy(event.getItem()));
++ // CraftBukkit end
+ if (entityliving instanceof Mob) {
+ Mob entityinsentient = (Mob) entityliving;
+
+@@ -42,6 +80,7 @@
+ entityinsentient.setPersistenceRequired();
+ }
+
++ if (shrink) stack.shrink(1); // Paper - shrink here
+ return true;
+ }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch
new file mode 100644
index 0000000000..ea22a6dd42
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch
@@ -0,0 +1,58 @@
+--- a/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java
++++ b/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java
+@@ -15,6 +15,11 @@
+ import net.minecraft.world.level.block.state.BlockState;
+ import net.minecraft.world.level.block.state.properties.RailShape;
+ import net.minecraft.world.phys.Vec3;
++// CraftBukkit start
++import org.bukkit.craftbukkit.block.CraftBlock;
++import org.bukkit.craftbukkit.inventory.CraftItemStack;
++import org.bukkit.event.block.BlockDispenseEvent;
++// CraftBukkit end
+
+ public class MinecartDispenseItemBehavior extends DefaultDispenseItemBehavior {
+
+@@ -62,11 +67,40 @@
+ }
+
+ Vec3 vec3d1 = new Vec3(d0, d1 + d3, d2);
+- AbstractMinecart entityminecartabstract = AbstractMinecart.createMinecart(worldserver, vec3d1.x, vec3d1.y, vec3d1.z, this.entityType, EntitySpawnReason.DISPENSER, stack, (Player) null);
++ // CraftBukkit start
++ // EntityMinecartAbstract entityminecartabstract = EntityMinecartAbstract.createMinecart(worldserver, vec3d1.x, vec3d1.y, vec3d1.z, this.entityType, EntitySpawnReason.DISPENSER, itemstack, (EntityHuman) null);
++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event
++ org.bukkit.block.Block block2 = CraftBlock.at(worldserver, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
+
++ BlockDispenseEvent event = new BlockDispenseEvent(block2, craftItem.clone(), new org.bukkit.util.Vector(vec3d1.x, vec3d1.y, vec3d1.z));
++ if (!DispenserBlock.eventFired) {
++ worldserver.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ // stack.grow(1); // Paper - shrink below
++ return stack;
++ }
++
++ boolean shrink = true; // Paper
++ if (!event.getItem().equals(craftItem)) {
++ shrink = false; // Paper - shrink below
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++
++ itemstack1 = CraftItemStack.asNMSCopy(event.getItem());
++ AbstractMinecart entityminecartabstract = AbstractMinecart.createMinecart(worldserver, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), this.entityType, EntitySpawnReason.DISPENSER, itemstack1, (Player) null);
++
+ if (entityminecartabstract != null) {
+- worldserver.addFreshEntity(entityminecartabstract);
+- stack.shrink(1);
++ if (worldserver.addFreshEntity(entityminecartabstract) && shrink) stack.shrink(1); // Paper - if entity add was successful and supposed to shrink
++ // CraftBukkit end
+ }
+
+ return stack;
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch
new file mode 100644
index 0000000000..2061a38275
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch
@@ -0,0 +1,58 @@
+--- a/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java
++++ b/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java
+@@ -8,6 +8,11 @@
+ import net.minecraft.world.item.ItemStack;
+ import net.minecraft.world.item.ProjectileItem;
+ import net.minecraft.world.level.block.DispenserBlock;
++// CraftBukkit start
++import org.bukkit.craftbukkit.block.CraftBlock;
++import org.bukkit.craftbukkit.inventory.CraftItemStack;
++import org.bukkit.event.block.BlockDispenseEvent;
++// CraftBukkit end
+
+ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior {
+
+@@ -31,8 +36,41 @@
+ Direction enumdirection = (Direction) pointer.state().getValue(DispenserBlock.FACING);
+ Position iposition = this.dispenseConfig.positionFunction().getDispensePosition(pointer, enumdirection);
+
+- Projectile.spawnProjectileUsingShoot(this.projectileItem.asProjectile(worldserver, iposition, stack, enumdirection), worldserver, stack, (double) enumdirection.getStepX(), (double) enumdirection.getStepY(), (double) enumdirection.getStepZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty());
+- stack.shrink(1);
++ // CraftBukkit start
++ // IProjectile.spawnProjectileUsingShoot(this.projectileItem.asProjectile(worldserver, iposition, itemstack, enumdirection), worldserver, itemstack, (double) enumdirection.getStepX(), (double) enumdirection.getStepY(), (double) enumdirection.getStepZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty()); // CraftBukkit - call when finish the BlockDispenseEvent
++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event
++ org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
++
++ BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) enumdirection.getStepX(), (double) enumdirection.getStepY(), (double) enumdirection.getStepZ()));
++ if (!DispenserBlock.eventFired) {
++ worldserver.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ // stack.grow(1); // Paper - shrink below
++ return stack;
++ }
++
++ boolean shrink = true; // Paper
++ if (!event.getItem().equals(craftItem)) {
++ shrink = false; // Paper - shrink below
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++
++ // SPIGOT-7923: Avoid create projectiles with empty item
++ if (!itemstack1.isEmpty()) {
++ Projectile iprojectile = Projectile.spawnProjectileUsingShoot(this.projectileItem.asProjectile(worldserver, iposition, CraftItemStack.unwrap(event.getItem()), enumdirection), worldserver, itemstack1, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty()); // Paper - track changed items in the dispense event; unwrap is safe here because all uses of the stack make their own copies
++ iprojectile.projectileSource = new org.bukkit.craftbukkit.projectiles.CraftBlockProjectileSource(pointer.blockEntity());
++ }
++ if (shrink) stack.shrink(1); // Paper - actually handle here
++ // CraftBukkit end
+ return stack;
+ }
+
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch
new file mode 100644
index 0000000000..bc579690dd
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch
@@ -0,0 +1,81 @@
+--- a/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java
++++ b/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java
+@@ -22,6 +22,12 @@
+ import net.minecraft.world.level.block.state.BlockState;
+ import net.minecraft.world.level.gameevent.GameEvent;
+ import net.minecraft.world.phys.AABB;
++// CraftBukkit start
++import org.bukkit.craftbukkit.block.CraftBlock;
++import org.bukkit.craftbukkit.event.CraftEventFactory;
++import org.bukkit.craftbukkit.inventory.CraftItemStack;
++import org.bukkit.event.block.BlockDispenseEvent;
++// CraftBukkit end
+
+ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior {
+
+@@ -30,11 +36,34 @@
+ @Override
+ protected ItemStack execute(BlockSource pointer, ItemStack stack) {
+ ServerLevel worldserver = pointer.level();
++ // CraftBukkit start
++ org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items
+
++ BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
++ if (!DispenserBlock.eventFired) {
++ worldserver.getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ return stack;
++ }
++
++ if (!event.getItem().equals(craftItem)) {
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++ // CraftBukkit end
++
+ if (!worldserver.isClientSide()) {
+ BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING));
+
+- this.setSuccess(ShearsDispenseItemBehavior.tryShearBeehive(worldserver, blockposition) || ShearsDispenseItemBehavior.tryShearLivingEntity(worldserver, blockposition, stack));
++ this.setSuccess(ShearsDispenseItemBehavior.tryShearBeehive(worldserver, blockposition) || ShearsDispenseItemBehavior.tryShearLivingEntity(worldserver, blockposition, stack, bukkitBlock, craftItem)); // CraftBukkit
+ if (this.isSuccess()) {
+ stack.hurtAndBreak(1, worldserver, (ServerPlayer) null, (item) -> {
+ });
+@@ -64,8 +93,8 @@
+ return false;
+ }
+
+- private static boolean tryShearLivingEntity(ServerLevel world, BlockPos pos, ItemStack shears) {
+- List list = world.getEntitiesOfClass(LivingEntity.class, new AABB(pos), EntitySelector.NO_SPECTATORS);
++ private static boolean tryShearLivingEntity(ServerLevel worldserver, BlockPos blockposition, ItemStack itemstack, org.bukkit.block.Block bukkitBlock, CraftItemStack craftItem) { // CraftBukkit - add args
++ List list = worldserver.getEntitiesOfClass(LivingEntity.class, new AABB(blockposition), EntitySelector.NO_SPECTATORS);
+ Iterator iterator = list.iterator();
+
+ while (iterator.hasNext()) {
+@@ -73,8 +102,16 @@
+
+ if (entityliving instanceof Shearable ishearable) {
+ if (ishearable.readyForShearing()) {
+- ishearable.shear(world, SoundSource.BLOCKS, shears);
+- world.gameEvent((Entity) null, (Holder) GameEvent.SHEAR, pos);
++ // CraftBukkit start
++ // Paper start - Add drops to shear events
++ org.bukkit.event.block.BlockShearEntityEvent event = CraftEventFactory.callBlockShearEntityEvent(entityliving, bukkitBlock, craftItem, ishearable.generateDefaultDrops(worldserver, itemstack));
++ if (event.isCancelled()) {
++ // Paper end - Add drops to shear events
++ continue;
++ }
++ // CraftBukkit end
++ ishearable.shear(worldserver, SoundSource.BLOCKS, itemstack, CraftItemStack.asNMSCopy(event.getDrops())); // Paper - Add drops to shear events
++ worldserver.gameEvent((Entity) null, (Holder) GameEvent.SHEAR, blockposition);
+ return true;
+ }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch
new file mode 100644
index 0000000000..bfa73fef42
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch
@@ -0,0 +1,54 @@
+--- a/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java
++++ b/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java
+@@ -10,6 +10,12 @@
+ import net.minecraft.world.level.block.DispenserBlock;
+ import org.slf4j.Logger;
+
++// CraftBukkit start
++import org.bukkit.craftbukkit.block.CraftBlock;
++import org.bukkit.craftbukkit.inventory.CraftItemStack;
++import org.bukkit.event.block.BlockDispenseEvent;
++// CraftBukkit end
++
+ public class ShulkerBoxDispenseBehavior extends OptionalDispenseItemBehavior {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+@@ -26,8 +32,37 @@
+ BlockPos blockposition = pointer.pos().relative(enumdirection);
+ Direction enumdirection1 = pointer.level().isEmptyBlock(blockposition.below()) ? enumdirection : Direction.UP;
+
++ // CraftBukkit start
++ org.bukkit.block.Block bukkitBlock = CraftBlock.at(pointer.level(), pointer.pos());
++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
++
++ BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ()));
++ if (!DispenserBlock.eventFired) {
++ pointer.level().getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ return stack;
++ }
++
++ if (!event.getItem().equals(craftItem)) {
++ // Chain to handler for new item
++ ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
++ DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++ idispensebehavior.dispense(pointer, eventStack);
++ return stack;
++ }
++ }
++ // CraftBukkit end
++
+ try {
+- this.setSuccess(((BlockItem) item).place(new DirectionalPlaceContext(pointer.level(), blockposition, enumdirection, stack, enumdirection1)).consumesAction());
++ // Paper start - track changed items in the dispense event
++ this.setSuccess(((BlockItem) item).place(new DirectionalPlaceContext(pointer.level(), blockposition, enumdirection, CraftItemStack.asNMSCopy(event.getItem()), enumdirection1)).consumesAction());
++ if (this.isSuccess()) {
++ stack.shrink(1); // vanilla shrink is in the place function above, manually handle it here
++ }
++ // Paper end - track changed items in the dispense event
+ } catch (Exception exception) {
+ ShulkerBoxDispenseBehavior.LOGGER.error("Error trying to place shulker box at {}", blockposition, exception);
+ }
diff --git a/paper-server/patches/sources/net/minecraft/core/registries/BuiltInRegistries.java.patch b/paper-server/patches/sources/net/minecraft/core/registries/BuiltInRegistries.java.patch
new file mode 100644
index 0000000000..d363add98b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/registries/BuiltInRegistries.java.patch
@@ -0,0 +1,52 @@
+--- a/net/minecraft/core/registries/BuiltInRegistries.java
++++ b/net/minecraft/core/registries/BuiltInRegistries.java
+@@ -296,6 +296,17 @@
+ public static final Registry> SLOT_DISPLAY = registerSimple(Registries.SLOT_DISPLAY, SlotDisplays::bootstrap);
+ public static final Registry RECIPE_BOOK_CATEGORY = registerSimple(Registries.RECIPE_BOOK_CATEGORY, RecipeBookCategories::bootstrap);
+ public static final Registry extends Registry>> REGISTRY = WRITABLE_REGISTRY;
++ // Paper start - add built-in registry conversions
++ public static final io.papermc.paper.registry.data.util.Conversions BUILT_IN_CONVERSIONS = new io.papermc.paper.registry.data.util.Conversions(new net.minecraft.resources.RegistryOps.RegistryInfoLookup() {
++ @Override
++ public java.util.Optional> lookup(final ResourceKey extends Registry extends T>> registryRef) {
++ final Registry registry = net.minecraft.server.RegistryLayer.STATIC_ACCESS.lookupOrThrow(registryRef);
++ return java.util.Optional.of(
++ new net.minecraft.resources.RegistryOps.RegistryInfo<>(registry, registry, Lifecycle.experimental())
++ );
++ }
++ });
++ // Paper end - add built-in registry conversions
+
+ private static Registry registerSimple(ResourceKey extends Registry> key, BuiltInRegistries.RegistryBootstrap initializer) {
+ return internalRegister(key, new MappedRegistry<>(key, Lifecycle.stable(), false), initializer);
+@@ -323,14 +334,22 @@
+ ResourceKey extends Registry> key, R registry, BuiltInRegistries.RegistryBootstrap initializer
+ ) {
+ Bootstrap.checkBootstrapCalled(() -> "registry " + key.location());
++ io.papermc.paper.registry.PaperRegistryAccess.instance().registerRegistry(registry.key(), registry); // Paper - initialize API registry
+ ResourceLocation resourceLocation = key.location();
+ LOADERS.put(resourceLocation, () -> initializer.run(registry));
+- WRITABLE_REGISTRY.register((ResourceKey>)key, registry, RegistrationInfo.BUILT_IN);
++ WRITABLE_REGISTRY.register((ResourceKey)key, registry, RegistrationInfo.BUILT_IN); // Paper - decompile fix
+ return registry;
+ }
+
+ public static void bootStrap() {
++ // Paper start
++ bootStrap(() -> {});
++ }
++ public static void bootStrap(Runnable runnable) {
++ // Paper end
++ REGISTRY.freeze(); // Paper - freeze main registry early
+ createContents();
++ runnable.run(); // Paper
+ freeze();
+ validate(REGISTRY);
+ }
+@@ -348,6 +367,7 @@
+
+ for (Registry> registry : REGISTRY) {
+ bindBootstrappedTagsToEmpty(registry);
++ io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.runFreezeListeners(registry.key(), BUILT_IN_CONVERSIONS); // Paper
+ registry.freeze();
+ }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/data/loot/packs/VanillaChestLoot.java.patch b/paper-server/patches/sources/net/minecraft/data/loot/packs/VanillaChestLoot.java.patch
new file mode 100644
index 0000000000..02e103aaf6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/data/loot/packs/VanillaChestLoot.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/data/loot/packs/VanillaChestLoot.java
++++ b/net/minecraft/data/loot/packs/VanillaChestLoot.java
+@@ -946,7 +946,6 @@
+ .add(
+ LootItem.lootTableItem(Items.COMPASS)
+ .apply(SetItemCountFunction.setCount(ConstantValue.exactly(1.0F)))
+- .apply(SetItemDamageFunction.setDamage(UniformGenerator.between(0.15F, 0.8F)))
+ .setWeight(1)
+ )
+ .add(LootItem.lootTableItem(Items.BUCKET).apply(SetItemCountFunction.setCount(UniformGenerator.between(1.0F, 2.0F))).setWeight(1))
diff --git a/paper-server/patches/sources/net/minecraft/nbt/ByteArrayTag.java.patch b/paper-server/patches/sources/net/minecraft/nbt/ByteArrayTag.java.patch
new file mode 100644
index 0000000000..d7c707925b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/nbt/ByteArrayTag.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/nbt/ByteArrayTag.java
++++ b/net/minecraft/nbt/ByteArrayTag.java
+@@ -1,3 +1,4 @@
++// mc-dev import
+ package net.minecraft.nbt;
+
+ import java.io.DataInput;
+@@ -24,6 +25,7 @@
+ private static byte[] readAccounted(DataInput input, NbtAccounter tracker) throws IOException {
+ tracker.accountBytes(24L);
+ int i = input.readInt();
++ com.google.common.base.Preconditions.checkArgument( i < 1 << 24); // Spigot
+
+ tracker.accountBytes(1L, (long) i);
+ byte[] abyte = new byte[i];
diff --git a/paper-server/patches/sources/net/minecraft/nbt/CompoundTag.java.patch b/paper-server/patches/sources/net/minecraft/nbt/CompoundTag.java.patch
new file mode 100644
index 0000000000..1cb8cb2d7b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/nbt/CompoundTag.java.patch
@@ -0,0 +1,74 @@
+--- a/net/minecraft/nbt/CompoundTag.java
++++ b/net/minecraft/nbt/CompoundTag.java
+@@ -49,7 +49,7 @@
+
+ private static CompoundTag loadCompound(DataInput input, NbtAccounter tracker) throws IOException {
+ tracker.accountBytes(48L);
+- Map map = Maps.newHashMap();
++ it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap map = new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>(8, 0.8f); // Paper - Reduce memory footprint of CompoundTag
+
+ byte b;
+ while ((b = input.readByte()) != 0) {
+@@ -166,7 +166,7 @@
+ }
+
+ public CompoundTag() {
+- this(Maps.newHashMap());
++ this(new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>(8, 0.8f)); // Paper - Reduce memory footprint of CompoundTag
+ }
+
+ @Override
+@@ -232,14 +232,34 @@
+ }
+
+ public void putUUID(String key, UUID value) {
++ // Paper start - Support old UUID format
++ if (this.contains(key + "Most", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC) && this.contains(key + "Least", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) {
++ this.tags.remove(key + "Most");
++ this.tags.remove(key + "Least");
++ }
++ // Paper end - Support old UUID format
+ this.tags.put(key, NbtUtils.createUUID(value));
+ }
+
++
++ /**
++ * You must use {@link #hasUUID(String)} before or else it will throw an NPE.
++ */
+ public UUID getUUID(String key) {
++ // Paper start - Support old UUID format
++ if (!contains(key, 11) && this.contains(key + "Most", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC) && this.contains(key + "Least", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) {
++ return new UUID(this.getLong(key + "Most"), this.getLong(key + "Least"));
++ }
++ // Paper end - Support old UUID format
+ return NbtUtils.loadUUID(this.get(key));
+ }
+
+ public boolean hasUUID(String key) {
++ // Paper start - Support old UUID format
++ if (this.contains(key + "Most", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC) && this.contains(key + "Least", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) {
++ return true;
++ }
++ // Paper end - Support old UUID format
+ Tag tag = this.get(key);
+ return tag != null && tag.getType() == IntArrayTag.TYPE && ((IntArrayTag)tag).getAsIntArray().length == 4;
+ }
+@@ -477,8 +497,16 @@
+
+ @Override
+ public CompoundTag copy() {
+- Map map = Maps.newHashMap(Maps.transformValues(this.tags, Tag::copy));
+- return new CompoundTag(map);
++ // Paper start - Reduce memory footprint of CompoundTag
++ it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap ret = new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>(this.tags.size(), 0.8f);
++ java.util.Iterator> iterator = (this.tags instanceof it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap) ? ((it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap)this.tags).object2ObjectEntrySet().fastIterator() : this.tags.entrySet().iterator();
++ while (iterator.hasNext()) {
++ Map.Entry entry = iterator.next();
++ ret.put(entry.getKey(), entry.getValue().copy());
++ }
++
++ return new CompoundTag(ret);
++ // Paper end - Reduce memory footprint of CompoundTag
+ }
+
+ @Override
diff --git a/paper-server/patches/sources/net/minecraft/nbt/IntArrayTag.java.patch b/paper-server/patches/sources/net/minecraft/nbt/IntArrayTag.java.patch
new file mode 100644
index 0000000000..97872e3339
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/nbt/IntArrayTag.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/nbt/IntArrayTag.java
++++ b/net/minecraft/nbt/IntArrayTag.java
+@@ -1,3 +1,4 @@
++// mc-dev import
+ package net.minecraft.nbt;
+
+ import java.io.DataInput;
+@@ -24,6 +25,7 @@
+ private static int[] readAccounted(DataInput input, NbtAccounter tracker) throws IOException {
+ tracker.accountBytes(24L);
+ int i = input.readInt();
++ com.google.common.base.Preconditions.checkArgument( i < 1 << 24); // Spigot
+
+ tracker.accountBytes(4L, (long) i);
+ int[] aint = new int[i];
diff --git a/paper-server/patches/sources/net/minecraft/nbt/NbtIo.java.patch b/paper-server/patches/sources/net/minecraft/nbt/NbtIo.java.patch
new file mode 100644
index 0000000000..f6dc9e632c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/nbt/NbtIo.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/nbt/NbtIo.java
++++ b/net/minecraft/nbt/NbtIo.java
+@@ -1,3 +1,4 @@
++// mc-dev import
+ package net.minecraft.nbt;
+
+ import java.io.BufferedOutputStream;
+@@ -324,6 +325,12 @@
+ }
+
+ public static CompoundTag read(DataInput input, NbtAccounter tracker) throws IOException {
++ // Spigot start
++ if ( input instanceof io.netty.buffer.ByteBufInputStream )
++ {
++ input = new DataInputStream(new org.spigotmc.LimitStream((InputStream) input, tracker));
++ }
++ // Spigot end
+ Tag nbtbase = NbtIo.readUnnamedTag(input, tracker);
+
+ if (nbtbase instanceof CompoundTag) {
diff --git a/paper-server/patches/sources/net/minecraft/nbt/NbtUtils.java.patch b/paper-server/patches/sources/net/minecraft/nbt/NbtUtils.java.patch
new file mode 100644
index 0000000000..a126b25999
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/nbt/NbtUtils.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/nbt/NbtUtils.java
++++ b/net/minecraft/nbt/NbtUtils.java
+@@ -149,8 +149,10 @@
+ if (!nbt.contains("Name", 8)) {
+ return Blocks.AIR.defaultBlockState();
+ } else {
+- ResourceLocation resourceLocation = ResourceLocation.parse(nbt.getString("Name"));
+- Optional extends Holder> optional = blockLookup.get(ResourceKey.create(Registries.BLOCK, resourceLocation));
++ // Paper start - Validate resource location
++ ResourceLocation resourceLocation = ResourceLocation.tryParse(nbt.getString("Name"));
++ Optional extends Holder> optional = resourceLocation != null ? blockLookup.get(ResourceKey.create(Registries.BLOCK, resourceLocation)) : Optional.empty();
++ // Paper end - Validate resource location
+ if (optional.isEmpty()) {
+ return Blocks.AIR.defaultBlockState();
+ } else {
diff --git a/paper-server/patches/sources/net/minecraft/nbt/TagParser.java.patch b/paper-server/patches/sources/net/minecraft/nbt/TagParser.java.patch
new file mode 100644
index 0000000000..dd6b67ed47
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/nbt/TagParser.java.patch
@@ -0,0 +1,69 @@
+--- a/net/minecraft/nbt/TagParser.java
++++ b/net/minecraft/nbt/TagParser.java
+@@ -49,6 +49,7 @@
+ }, CompoundTag::toString);
+ public static final Codec LENIENT_CODEC = Codec.withAlternative(AS_CODEC, CompoundTag.CODEC);
+ private final StringReader reader;
++ private int depth; // Paper
+
+ public static CompoundTag parseTag(String string) throws CommandSyntaxException {
+ return new TagParser(new StringReader(string)).readSingleStruct();
+@@ -159,6 +160,7 @@
+
+ public CompoundTag readStruct() throws CommandSyntaxException {
+ this.expect('{');
++ this.increaseDepth(); // Paper
+ CompoundTag compoundTag = new CompoundTag();
+ this.reader.skipWhitespace();
+
+@@ -182,6 +184,7 @@
+ }
+
+ this.expect('}');
++ this.depth--; // Paper
+ return compoundTag;
+ }
+
+@@ -191,6 +194,7 @@
+ if (!this.reader.canRead()) {
+ throw ERROR_EXPECTED_VALUE.createWithContext(this.reader);
+ } else {
++ this.increaseDepth(); // Paper
+ ListTag listTag = new ListTag();
+ TagType> tagType = null;
+
+@@ -216,6 +220,7 @@
+ }
+
+ this.expect(']');
++ this.depth--; // Paper
+ return listTag;
+ }
+ }
+@@ -253,11 +258,11 @@
+ }
+
+ if (typeReader == ByteTag.TYPE) {
+- list.add((T)((NumericTag)tag).getAsByte());
++ list.add((T)(Byte)((NumericTag)tag).getAsByte()); // Paper - decompile fix
+ } else if (typeReader == LongTag.TYPE) {
+- list.add((T)((NumericTag)tag).getAsLong());
++ list.add((T)(Long)((NumericTag)tag).getAsLong()); // Paper - decompile fix
+ } else {
+- list.add((T)((NumericTag)tag).getAsInt());
++ list.add((T)(Integer)((NumericTag)tag).getAsInt()); // Paper - decompile fix
+ }
+
+ if (!this.hasElementSeparator()) {
+@@ -288,4 +293,11 @@
+ this.reader.skipWhitespace();
+ this.reader.expect(c);
+ }
++
++ private void increaseDepth() throws CommandSyntaxException {
++ this.depth++;
++ if (this.depth > 512) {
++ throw new io.papermc.paper.brigadier.TagParseCommandSyntaxException("NBT tag is too complex, depth > 512");
++ }
++ }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/network/Connection.java.patch b/paper-server/patches/sources/net/minecraft/network/Connection.java.patch
new file mode 100644
index 0000000000..f1e414f6b1
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/Connection.java.patch
@@ -0,0 +1,330 @@
+--- a/net/minecraft/network/Connection.java
++++ b/net/minecraft/network/Connection.java
+@@ -82,13 +82,13 @@
+ marker.add(Connection.PACKET_MARKER);
+ });
+ public static final Supplier NETWORK_WORKER_GROUP = Suppliers.memoize(() -> {
+- return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Client IO #%d").setDaemon(true).build());
++ return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
+ });
+ public static final Supplier NETWORK_EPOLL_WORKER_GROUP = Suppliers.memoize(() -> {
+- return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).build());
++ return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
+ });
+ public static final Supplier LOCAL_WORKER_GROUP = Suppliers.memoize(() -> {
+- return new DefaultEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Local Client IO #%d").setDaemon(true).build());
++ return new DefaultEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Local Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
+ });
+ private static final ProtocolInfo INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND;
+ private final PacketFlow receiving;
+@@ -96,6 +96,11 @@
+ private final Queue> pendingActions = Queues.newConcurrentLinkedQueue();
+ public Channel channel;
+ public SocketAddress address;
++ // Spigot Start
++ public java.util.UUID spoofedUUID;
++ public com.mojang.authlib.properties.Property[] spoofedProfile;
++ public boolean preparing = true;
++ // Spigot End
+ @Nullable
+ private volatile PacketListener disconnectListener;
+ @Nullable
+@@ -114,7 +119,42 @@
+ private volatile DisconnectionDetails delayedDisconnect;
+ @Nullable
+ BandwidthDebugMonitor bandwidthDebugMonitor;
++ public String hostname = ""; // CraftBukkit - add field
++ // Paper start - NetworkClient implementation
++ public int protocolVersion;
++ public java.net.InetSocketAddress virtualHost;
++ private static boolean enableExplicitFlush = Boolean.getBoolean("paper.explicit-flush"); // Paper - Disable explicit network manager flushing
++ // Paper end
+
++ // Paper start - add utility methods
++ public final net.minecraft.server.level.ServerPlayer getPlayer() {
++ if (this.packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl impl) {
++ return impl.player;
++ } else if (this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl impl) {
++ org.bukkit.craftbukkit.entity.CraftPlayer player = impl.getCraftPlayer();
++ return player == null ? null : player.getHandle();
++ }
++ return null;
++ }
++ // Paper end - add utility methods
++ // Paper start - packet limiter
++ protected final Object PACKET_LIMIT_LOCK = new Object();
++ protected final @Nullable io.papermc.paper.util.IntervalledCounter allPacketCounts = io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.allPackets.isEnabled() ? new io.papermc.paper.util.IntervalledCounter(
++ (long)(io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.allPackets.interval() * 1.0e9)
++ ) : null;
++ protected final java.util.Map>, io.papermc.paper.util.IntervalledCounter> packetSpecificLimits = new java.util.HashMap<>();
++
++ private boolean stopReadingPackets;
++ private void killForPacketSpam() {
++ this.sendPacket(new ClientboundDisconnectPacket(io.papermc.paper.adventure.PaperAdventure.asVanilla(io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.kickMessage)), PacketSendListener.thenRun(() -> {
++ this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.kickMessage));
++ }), true);
++ this.setReadOnly();
++ this.stopReadingPackets = true;
++ }
++ // Paper end - packet limiter
++ @Nullable public SocketAddress haProxyAddress; // Paper - Add API to get player's proxy address
++
+ public Connection(PacketFlow side) {
+ this.receiving = side;
+ }
+@@ -123,6 +163,9 @@
+ super.channelActive(channelhandlercontext);
+ this.channel = channelhandlercontext.channel();
+ this.address = this.channel.remoteAddress();
++ // Spigot Start
++ this.preparing = false;
++ // Spigot End
+ if (this.delayedDisconnect != null) {
+ this.disconnect(this.delayedDisconnect);
+ }
+@@ -134,6 +177,21 @@
+ }
+
+ public void exceptionCaught(ChannelHandlerContext channelhandlercontext, Throwable throwable) {
++ // Paper start - Handle large packets disconnecting client
++ if (throwable instanceof io.netty.handler.codec.EncoderException && throwable.getCause() instanceof PacketEncoder.PacketTooLargeException packetTooLargeException) {
++ final Packet> packet = packetTooLargeException.getPacket();
++ if (packet.packetTooLarge(this)) {
++ ProtocolSwapHandler.handleOutboundTerminalPacket(channelhandlercontext, packet);
++ return;
++ } else if (packet.isSkippable()) {
++ Connection.LOGGER.debug("Skipping packet due to errors", throwable.getCause());
++ ProtocolSwapHandler.handleOutboundTerminalPacket(channelhandlercontext, packet);
++ return;
++ } else {
++ throwable = throwable.getCause();
++ }
++ }
++ // Paper end - Handle large packets disconnecting client
+ if (throwable instanceof SkipPacketException) {
+ Connection.LOGGER.debug("Skipping packet due to errors", throwable.getCause());
+ } else {
+@@ -141,8 +199,10 @@
+
+ this.handlingFault = true;
+ if (this.channel.isOpen()) {
++ net.minecraft.server.level.ServerPlayer player = this.getPlayer(); // Paper - Add API for quit reason
+ if (throwable instanceof TimeoutException) {
+ Connection.LOGGER.debug("Timeout", throwable);
++ if (player != null) player.quitReason = org.bukkit.event.player.PlayerQuitEvent.QuitReason.TIMED_OUT; // Paper - Add API for quit reason
+ this.disconnect((Component) Component.translatable("disconnect.timeout"));
+ } else {
+ MutableComponent ichatmutablecomponent = Component.translatable("disconnect.genericReason", "Internal Exception: " + String.valueOf(throwable));
+@@ -155,9 +215,11 @@
+ disconnectiondetails = new DisconnectionDetails(ichatmutablecomponent);
+ }
+
++ if (player != null) player.quitReason = org.bukkit.event.player.PlayerQuitEvent.QuitReason.ERRONEOUS_STATE; // Paper - Add API for quit reason
+ if (flag) {
+ Connection.LOGGER.debug("Failed to sent packet", throwable);
+- if (this.getSending() == PacketFlow.CLIENTBOUND) {
++ boolean doesDisconnectExist = this.packetListener.protocol() != ConnectionProtocol.STATUS && this.packetListener.protocol() != ConnectionProtocol.HANDSHAKING; // Paper
++ if (this.getSending() == PacketFlow.CLIENTBOUND && doesDisconnectExist) { // Paper
+ Packet> packet = this.sendLoginDisconnect ? new ClientboundLoginDisconnectPacket(ichatmutablecomponent) : new ClientboundDisconnectPacket(ichatmutablecomponent);
+
+ this.send((Packet) packet, PacketSendListener.thenRun(() -> {
+@@ -176,6 +238,7 @@
+
+ }
+ }
++ if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) io.papermc.paper.util.TraceUtil.printStackTrace(throwable); // Spigot // Paper
+ }
+
+ protected void channelRead0(ChannelHandlerContext channelhandlercontext, Packet> packet) {
+@@ -185,11 +248,61 @@
+ if (packetlistener == null) {
+ throw new IllegalStateException("Received a packet before the packet listener was initialized");
+ } else {
++ // Paper start - packet limiter
++ if (this.stopReadingPackets) {
++ return;
++ }
++ if (this.allPacketCounts != null ||
++ io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.overrides.containsKey(packet.getClass())) {
++ long time = System.nanoTime();
++ synchronized (PACKET_LIMIT_LOCK) {
++ if (this.allPacketCounts != null) {
++ this.allPacketCounts.updateAndAdd(1, time);
++ if (this.allPacketCounts.getRate() >= io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.allPackets.maxPacketRate()) {
++ this.killForPacketSpam();
++ return;
++ }
++ }
++
++ for (Class> check = packet.getClass(); check != Object.class; check = check.getSuperclass()) {
++ io.papermc.paper.configuration.GlobalConfiguration.PacketLimiter.PacketLimit packetSpecificLimit =
++ io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.overrides.get(check);
++ if (packetSpecificLimit == null || !packetSpecificLimit.isEnabled()) {
++ continue;
++ }
++ io.papermc.paper.util.IntervalledCounter counter = this.packetSpecificLimits.computeIfAbsent((Class)check, (clazz) -> {
++ return new io.papermc.paper.util.IntervalledCounter((long)(packetSpecificLimit.interval() * 1.0e9));
++ });
++ counter.updateAndAdd(1, time);
++ if (counter.getRate() >= packetSpecificLimit.maxPacketRate()) {
++ switch (packetSpecificLimit.action()) {
++ case DROP:
++ return;
++ case KICK:
++ String deobfedPacketName = io.papermc.paper.util.ObfHelper.INSTANCE.deobfClassName(check.getName());
++
++ String playerName;
++ if (this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl impl) {
++ playerName = impl.getOwner().getName();
++ } else {
++ playerName = this.getLoggableAddress(net.minecraft.server.MinecraftServer.getServer().logIPs());
++ }
++
++ Connection.LOGGER.warn("{} kicked for packet spamming: {}", playerName, deobfedPacketName.substring(deobfedPacketName.lastIndexOf(".") + 1));
++ this.killForPacketSpam();
++ return;
++ }
++ }
++ }
++ }
++ }
++ // Paper end - packet limiter
+ if (packetlistener.shouldHandleMessage(packet)) {
+ try {
+ Connection.genericsFtw(packet, packetlistener);
+ } catch (RunningOnDifferentThreadException cancelledpackethandleexception) {
+ ;
++ } catch (io.papermc.paper.util.ServerStopRejectedExecutionException ignored) { // Paper - do not prematurely disconnect players on stop
+ } catch (RejectedExecutionException rejectedexecutionexception) {
+ this.disconnect((Component) Component.translatable("multiplayer.disconnect.server_shutdown"));
+ } catch (ClassCastException classcastexception) {
+@@ -205,7 +318,7 @@
+ }
+
+ private static void genericsFtw(Packet packet, PacketListener listener) {
+- packet.handle(listener);
++ packet.handle((T) listener); // CraftBukkit - decompile error
+ }
+
+ private void validateListener(ProtocolInfo> state, PacketListener listener) {
+@@ -418,12 +531,26 @@
+ }
+ }
+
++ private static final int MAX_PER_TICK = io.papermc.paper.configuration.GlobalConfiguration.get().misc.maxJoinsPerTick; // Paper - Buffer joins to world
++ private static int joinAttemptsThisTick; // Paper - Buffer joins to world
++ private static int currTick; // Paper - Buffer joins to world
+ public void tick() {
+ this.flushQueue();
++ // Paper start - Buffer joins to world
++ if (Connection.currTick != net.minecraft.server.MinecraftServer.currentTick) {
++ Connection.currTick = net.minecraft.server.MinecraftServer.currentTick;
++ Connection.joinAttemptsThisTick = 0;
++ }
++ // Paper end - Buffer joins to world
+ PacketListener packetlistener = this.packetListener;
+
+ if (packetlistener instanceof TickablePacketListener tickablepacketlistener) {
++ // Paper start - Buffer joins to world
++ if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener)
++ || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING
++ || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) {
+ tickablepacketlistener.tick();
++ } // Paper end - Buffer joins to world
+ }
+
+ if (!this.isConnected() && !this.disconnectionHandled) {
+@@ -431,7 +558,7 @@
+ }
+
+ if (this.channel != null) {
+- this.channel.flush();
++ if (enableExplicitFlush) this.channel.eventLoop().execute(() -> this.channel.flush()); // Paper - Disable explicit network manager flushing; we don't need to explicit flush here, but allow opt in incase issues are found to a better version
+ }
+
+ if (this.tickCount++ % 20 == 0) {
+@@ -464,12 +591,15 @@
+ }
+
+ public void disconnect(DisconnectionDetails disconnectionInfo) {
++ // Spigot Start
++ this.preparing = false;
++ // Spigot End
+ if (this.channel == null) {
+ this.delayedDisconnect = disconnectionInfo;
+ }
+
+ if (this.isConnected()) {
+- this.channel.close().awaitUninterruptibly();
++ this.channel.close(); // We can't wait as this may be called from an event loop.
+ this.disconnectionDetails = disconnectionInfo;
+ }
+
+@@ -537,7 +667,7 @@
+ }
+
+ public void configurePacketHandler(ChannelPipeline pipeline) {
+- pipeline.addLast("hackfix", new ChannelOutboundHandlerAdapter(this) {
++ pipeline.addLast("hackfix", new ChannelOutboundHandlerAdapter() { // CraftBukkit - decompile error
+ public void write(ChannelHandlerContext channelhandlercontext, Object object, ChannelPromise channelpromise) throws Exception {
+ super.write(channelhandlercontext, object, channelpromise);
+ }
+@@ -613,6 +743,14 @@
+
+ }
+
++ // Paper start - add proper async disconnect
++ public void enableAutoRead() {
++ if (this.channel != null) {
++ this.channel.config().setAutoRead(true);
++ }
++ }
++ // Paper end - add proper async disconnect
++
+ public void setupCompression(int compressionThreshold, boolean rejectsBadPackets) {
+ if (compressionThreshold >= 0) {
+ ChannelHandler channelhandler = this.channel.pipeline().get("decompress");
+@@ -633,6 +771,7 @@
+ } else {
+ this.channel.pipeline().addAfter("prepender", "compress", new CompressionEncoder(compressionThreshold));
+ }
++ this.channel.pipeline().fireUserEventTriggered(io.papermc.paper.network.ConnectionEvent.COMPRESSION_THRESHOLD_SET); // Paper - Add Channel initialization listeners
+ } else {
+ if (this.channel.pipeline().get("decompress") instanceof CompressionDecoder) {
+ this.channel.pipeline().remove("decompress");
+@@ -641,6 +780,7 @@
+ if (this.channel.pipeline().get("compress") instanceof CompressionEncoder) {
+ this.channel.pipeline().remove("compress");
+ }
++ this.channel.pipeline().fireUserEventTriggered(io.papermc.paper.network.ConnectionEvent.COMPRESSION_DISABLED); // Paper - Add Channel initialization listeners
+ }
+
+ }
+@@ -661,6 +801,27 @@
+
+ packetlistener1.onDisconnect(disconnectiondetails);
+ }
++ this.pendingActions.clear(); // Free up packet queue.
++ // Paper start - Add PlayerConnectionCloseEvent
++ final PacketListener packetListener = this.getPacketListener();
++ if (packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) {
++ /* Player was logged in, either game listener or configuration listener */
++ final com.mojang.authlib.GameProfile profile = commonPacketListener.getOwner();
++ new com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent(profile.getId(),
++ profile.getName(), ((InetSocketAddress) this.address).getAddress(), false).callEvent();
++ } else if (packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginListener) {
++ /* Player is login stage */
++ switch (loginListener.state) {
++ case VERIFYING:
++ case WAITING_FOR_DUPE_DISCONNECT:
++ case PROTOCOL_SWITCHING:
++ case ACCEPTED:
++ final com.mojang.authlib.GameProfile profile = loginListener.authenticatedProfile; /* Should be non-null at this stage */
++ new com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent(profile.getId(), profile.getName(),
++ ((InetSocketAddress) this.address).getAddress(), false).callEvent();
++ }
++ }
++ // Paper end - Add PlayerConnectionCloseEvent
+
+ }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/network/FriendlyByteBuf.java.patch b/paper-server/patches/sources/net/minecraft/network/FriendlyByteBuf.java.patch
new file mode 100644
index 0000000000..c9d512f8d6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/FriendlyByteBuf.java.patch
@@ -0,0 +1,105 @@
+--- a/net/minecraft/network/FriendlyByteBuf.java
++++ b/net/minecraft/network/FriendlyByteBuf.java
+@@ -72,6 +72,7 @@
+
+ public static final int DEFAULT_NBT_QUOTA = 2097152;
+ private final ByteBuf source;
++ @Nullable public final java.util.Locale adventure$locale; // Paper - track player's locale for server-side translations
+ public static final short MAX_STRING_LENGTH = Short.MAX_VALUE;
+ public static final int MAX_COMPONENT_STRING_LENGTH = 262144;
+ private static final int PUBLIC_KEY_SIZE = 256;
+@@ -80,6 +81,7 @@
+ private static final Gson GSON = new Gson();
+
+ public FriendlyByteBuf(ByteBuf parent) {
++ this.adventure$locale = PacketEncoder.ADVENTURE_LOCALE.get(); // Paper - track player's locale for server-side translations
+ this.source = parent;
+ }
+
+@@ -120,11 +122,16 @@
+ }
+
+ public void writeJsonWithCodec(Codec codec, T value) {
++ // Paper start - Adventure; add max length parameter
++ this.writeJsonWithCodec(codec, value, MAX_STRING_LENGTH);
++ }
++ public void writeJsonWithCodec(Codec codec, T value, int maxLength) {
++ // Paper end - Adventure; add max length parameter
+ DataResult