diff --git a/.gitignore b/.gitignore
index 744289df..87d4436c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,113 @@
-# Project exclude paths
-/target/
\ No newline at end of file
+# User-specific stuff
+.idea/
+
+*.iml
+*.ipr
+*.iws
+
+# IntelliJ
+out/
+
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+target/
+
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+.flattened-pom.xml
+
+# Common working directory
+run/
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..f288702d
--- /dev/null
+++ b/LICENSE
@@ -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/README.md b/README.md
index 4ce37c00..f0d523c1 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,36 @@
# StackMob-5
-StackMob redefined for 1.14 only. Uses NBT rather than databases/yaml files so performance will be greatly improved.
+StackMob for 1.20.4+. StackMob 5 uses Bukkit's PersistentDataContainer API so performance will be greatly improved from previous versions.
+
+Spigot: https://www.spigotmc.org/resources/stackmob-enhance-your-servers-performance-without-the-sacrifice.29999/
+
+StackMob is a plugin that reduces the strain of mobs on your server by 'stacking' them together, therefore you can have 1 entity instead of 30 entities. StackMob aims to try and not break vanilla mechanics, however this is not always pratical and/or possible.
+
+It is recommended to use StackMob in conjunction with a farm limiter plugin for the best results.
+
+### Jenkins: https://ci.codemc.io/job/Nathat23/job/StackMob-5/
+# Building
+In order to build StackMob, you will need to do the following.
+- Clone the repository.
+- Run 'mvn clean install' to compile.
+
+# Contributing
+Contributions are welcome. StackMob is licensed under the GPLv3.
+
+# API
+StackMob does have a few custom events that can be used. These are in the events subpackage.
+
+Maven:
+```xml
+
+ CodeMC
+ https://repo.codemc.org/repository/maven-public/
+
+```
+```xml
+
+ uk.antiperson.stackmob
+ StackMob
+ 5.8.2
+
+```
diff --git a/jars/Clearlag.jar b/jars/Clearlag.jar
new file mode 100644
index 00000000..12f307c2
Binary files /dev/null and b/jars/Clearlag.jar differ
diff --git a/jars/MyPet-3.11-SNAPSHOT-B1627.jar b/jars/MyPet-3.11-SNAPSHOT-B1627.jar
new file mode 100644
index 00000000..9afd98b1
Binary files /dev/null and b/jars/MyPet-3.11-SNAPSHOT-B1627.jar differ
diff --git a/jars/MythicMobs-4.9.0.jar b/jars/MythicMobs-4.9.0.jar
deleted file mode 100755
index 0a3a6860..00000000
Binary files a/jars/MythicMobs-4.9.0.jar and /dev/null differ
diff --git a/pom.xml b/pom.xml
index ac3c083f..86add097 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
uk.antiperson.stackmobStackMob
- 5.2.0
+ 5.10.3src/main/java
@@ -33,27 +33,31 @@
1.3
+
+ UTF-8
+ org.apache.maven.pluginsmaven-compiler-plugin
- 3.7.0
+ 3.10.1
- 1.8
- 1.8
+ 17
+ 17org.apache.maven.pluginsmaven-shade-plugin
- 3.1.0
+ 3.5.3org.bstats
- uk.antiperson.stackmob
+ uk.antiperson.stackmob.libs.bstats
+ true
@@ -67,79 +71,100 @@
-
+
+
+ apache.snapshots
+ https://repository.apache.org/snapshots/
+
+
+
+
- dmulloy2-repo
- https://repo.dmulloy2.net/nexus/repository/public/
+ codemc-releases
+ https://repo.codemc.io/repository/maven-releases/
+
+ codemc-snapshots
+ https://repo.codemc.io/repository/maven-snapshots/
+
+
+
+
- sk89q-repo
- http://maven.sk89q.com/repo/
+ enginehub
+ https://maven.enginehub.org/repo/papermc
- https://papermc.io/repo/repository/maven-public/
-
-
- CodeMC
- https://repo.codemc.org/repository/maven-public
+ https://repo.papermc.io/repository/maven-public/jitpack.iohttps://jitpack.io
+
+ lumine-repo
+ https://mvn.lumine.io/repository/maven-public/
+
- com.destroystokyo.paper
+ io.papermc.paperpaper-api
- 1.16.1-R0.1-SNAPSHOT
+ 1.21.1-R0.1-SNAPSHOTprovided
- org.spigotmc
- spigot
- 1.16.1-R0.1-SNAPSHOT
+ dev.folia
+ folia-api
+ 1.20.6-R0.1-SNAPSHOTprovidedcom.sk89q.worldguard
- worldguard-legacy
- 7.0.0-SNAPSHOT
+ worldguard-bukkit
+ 7.1.0-SNAPSHOTprovidedcom.sk89q.worldeditworldedit-bukkit
- 7.1.0-SNAPSHOT
+ 7.2.16-SNAPSHOTprovidedorg.bstatsbstats-bukkit
- 1.5
+ 3.0.2compile
- io.lumine.xikage.mythicmobs
- mythicmobs
- 4.9.0
- system
- ${project.basedir}/jars/MythicMobs-4.9.0.jar
+ io.lumine
+ Mythic-Dist
+ 5.1.2-SNAPSHOT
+ providedcom.github.ZripsJobs
- LATEST
+ v5.2.2.3provided
- com.comphenix.protocol
- ProtocolLib
- 4.5.1
- provided
+ me.minebuilders
+ clearlag
+ 3.2.2
+ system
+ ${project.basedir}/jars/Clearlag.jar
+
+
+ de.keyle
+ mypet
+ 3.11
+ system
+ ${project.basedir}/jars/MyPet-3.11-SNAPSHOT-B1627.jar
-
\ No newline at end of file
+
diff --git a/src/main/java/uk/antiperson/stackmob/StackMob.java b/src/main/java/uk/antiperson/stackmob/StackMob.java
index bdfff6c9..89808b32 100644
--- a/src/main/java/uk/antiperson/stackmob/StackMob.java
+++ b/src/main/java/uk/antiperson/stackmob/StackMob.java
@@ -5,23 +5,24 @@
import org.bukkit.NamespacedKey;
import org.bukkit.command.PluginCommand;
import org.bukkit.event.Listener;
-import org.bukkit.plugin.InvalidPluginException;
-import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import uk.antiperson.stackmob.commands.Commands;
import uk.antiperson.stackmob.config.EntityTranslation;
import uk.antiperson.stackmob.config.MainConfig;
import uk.antiperson.stackmob.entity.EntityManager;
+import uk.antiperson.stackmob.entity.StackEntity;
+import uk.antiperson.stackmob.entity.tags.DisplayTagListeners;
import uk.antiperson.stackmob.entity.traits.TraitManager;
import uk.antiperson.stackmob.hook.HookManager;
import uk.antiperson.stackmob.listeners.*;
+import uk.antiperson.stackmob.scheduler.BukkitScheduler;
+import uk.antiperson.stackmob.scheduler.FoliaScheduler;
+import uk.antiperson.stackmob.scheduler.Scheduler;
import uk.antiperson.stackmob.tasks.MergeTask;
-import uk.antiperson.stackmob.tasks.TagTask;
import uk.antiperson.stackmob.utils.ItemTools;
import uk.antiperson.stackmob.utils.Updater;
import uk.antiperson.stackmob.utils.Utilities;
-import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.logging.Level;
@@ -29,7 +30,6 @@
public class StackMob extends JavaPlugin {
private final NamespacedKey stackKey = new NamespacedKey(this, "stack-size");
- private final NamespacedKey waitKey = new NamespacedKey(this, "wait-key");
private final NamespacedKey toolKey = new NamespacedKey(this, "stack-tool");
private MainConfig config;
@@ -39,9 +39,22 @@ public class StackMob extends JavaPlugin {
private EntityManager entityManager;
private Updater updater;
private ItemTools itemTools;
+ private Scheduler scheduler;
+
+ private boolean stepDamageError;
@Override
public void onLoad() {
+ if (!Utilities.isPaper()) {
+ getLogger().severe("It has been detected that you are not using Paper (https://papermc.io).");
+ getLogger().severe("StackMob makes use of Paper's API, which means this version of the plugin will not work. Disabling.");
+ getServer().getPluginManager().disablePlugin(this);
+ }
+ if (!Utilities.isVersionAtLeast(Utilities.MinecraftVersion.V1_20_4)) {
+ getLogger().severe("Unsupported Minecraft version: " + Utilities.getMinecraftVersion());
+ getLogger().severe("We are now disabling. Please find the appropriate plugin version for your server setup.");
+ getServer().getPluginManager().disablePlugin(this);
+ }
hookManager = new HookManager(this);
try {
hookManager.registerOnLoad();
@@ -49,25 +62,32 @@ public void onLoad() {
getLogger().log(Level.SEVERE, "There was a problem registering hooks. Features won't work.");
e.printStackTrace();
}
+ scheduler = Utilities.IS_FOLIA ? new FoliaScheduler(this) : new BukkitScheduler(this);
}
@Override
public void onEnable() {
- getLogger().info("StackMob v" + getDescription().getVersion() + " by antiPerson and contributors.");
- getLogger().info("GitHub: " + Utilities.GITHUB);
- getLogger().info("Discord: " + Utilities.DISCORD);
traitManager = new TraitManager(this);
entityManager = new EntityManager(this);
config = new MainConfig(this);
entityTranslation = new EntityTranslation(this);
+ updater = new Updater(this, "stackmob");
+ itemTools = new ItemTools(this);
+ getLogger().info("StackMob v" + getDescription().getVersion() + " by antiPerson and contributors.");
+ getLogger().info("GitHub: " + Utilities.GITHUB + " Discord: " + Utilities.DISCORD);
getLogger().info("Loading config files...");
- loadConfig();
+ try {
+ getMainConfig().init();
+ getEntityTranslation().reloadConfig();
+ } catch (IOException e) {
+ getLogger().log(Level.SEVERE, "There was a problem loading the configuration file.");
+ e.printStackTrace();
+ }
getLogger().info("Registering hooks and trait checks...");
- try{
- getTraitManager().registerTraits();
+ try {
getHookManager().registerHooks();
- } catch (IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
- getLogger().log(Level.SEVERE, "There was a problem registering traits and hooks. Features won't work.");
+ getTraitManager().registerTraits();
+ } catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
e.printStackTrace();
}
getLogger().info("Registering events, commands and tasks...");
@@ -76,57 +96,36 @@ public void onEnable() {
} catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
- register();
- updater = new Updater(this, 29999);
+ PluginCommand command = getCommand("stackmob");
+ Commands commands = new Commands(this);
+ command.setExecutor(commands);
+ command.setTabCompleter(commands);
+ commands.registerSubCommands();
+ int stackInterval = getMainConfig().getConfig().getStackInterval();
+ getScheduler().runGlobalTaskTimer(new MergeTask(this), 20, stackInterval);
+ if (getMainConfig().getConfig().isUseArmorStand() && getMainConfig().getConfig().getTagMode() == StackEntity.TagMode.NEARBY) {
+ getServer().getPluginManager().registerEvents(new DisplayTagListeners(this), this);
+ }
+ getLogger().info("Detected server version " + Utilities.getMinecraftVersion());
+ getEntityManager().registerAllEntities();
getUpdater().checkUpdate().whenComplete(((updateResult, throwable) -> {
switch (updateResult.getResult()) {
- case NONE:
- getLogger().info("No update is currently available.");
- break;
- case ERROR:
- getLogger().info("There was an error while getting the latest update.");
- break;
- case AVAILABLE:
- getLogger().info("A new version is currently available. (" + updateResult.getNewVersion() + ")");
- break;
+ case NONE: getLogger().info("No update is currently available."); break;
+ case ERROR: getLogger().info("There was an error while getting the latest update."); break;
+ case AVAILABLE: getLogger().info("A new version is currently available. (" + updateResult.getNewVersion() + ")"); break;
}
}));
- Metrics metrics = new Metrics(this);
- metrics.addCustomChart(new Metrics.SimplePie("stackmobbridge", () -> String.valueOf(Bukkit.getPluginManager().isPluginEnabled("StackMobBridge"))));
- if (metrics.isEnabled()) {
- getLogger().info("bStats anonymous data collection has been enabled!");
- }
- itemTools = new ItemTools(this);
- }
- private void loadConfig() {
- try {
- getMainConfig().load();
- getEntityTranslation().load();
- } catch (IOException e) {
- getLogger().log(Level.SEVERE, "There was a problem loading the configuration file. Features won't work.");
- e.printStackTrace();
- }
+ new Metrics(this, 522);
}
- private void register() {
- int stackInterval = getMainConfig().getStackInterval();
- new MergeTask(this).runTaskTimer(this, 5, stackInterval);
- if (Utilities.isNewBukkit() || getHookManager().getProtocolLibHook() != null) {
- int tagInterval = getMainConfig().getTagNearbyInterval();
- new TagTask(this).runTaskTimer(this, 5, tagInterval);
- } else {
- getLogger().warning("You are not running the plugins native version and ProtocolLib could not be found (or has been disabled).");
- getLogger().warning("The display name visibility setting 'NEARBY' will not work unless this is fixed.");
- }
- PluginCommand command = getCommand("stackmob");
- Commands commands = new Commands(this);
- command.setExecutor(commands);
- command.setTabCompleter(commands);
- commands.registerSubCommands();
+ @Override
+ public void onDisable() {
+ getEntityManager().unregisterAllEntities();
}
private void registerEvents() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
+ registerEvent(BucketListener.class);
registerEvent(DeathListener.class);
registerEvent(TransformListener.class);
registerEvent(BreedInteractListener.class);
@@ -140,15 +139,26 @@ private void registerEvents() throws InvocationTargetException, NoSuchMethodExce
registerEvent(SpawnListener.class);
registerEvent(TargetListener.class);
registerEvent(PlayerListener.class);
+ registerEvent(BeeListener.class);
+ registerEvent(LeashListener.class);
+ registerEvent(EquipListener.class);
+ if (Utilities.isVersionAtLeast(Utilities.MinecraftVersion.V1_20_4)) {
+ registerEvent(KnockbackListener.class);
+ }
+ if (Utilities.isPaper()) {
+ registerEvent(RemoveListener.class);
+ return;
+ }
+ registerEvent(ChunkListener.class);
}
private void registerEvent(Class extends Listener> clazz) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
ListenerMetadata listenerMetadata = clazz.getAnnotation(ListenerMetadata.class);
if (listenerMetadata != null) {
- if (!getMainConfig().isSet(listenerMetadata.config())) {
+ if (!getMainConfig().getConfigFile().isSet(listenerMetadata.config())) {
return;
}
- if (!getMainConfig().getBoolean(listenerMetadata.config())) {
+ if (!getMainConfig().getConfigFile().getBoolean(listenerMetadata.config())) {
return;
}
}
@@ -156,28 +166,6 @@ private void registerEvent(Class extends Listener> clazz) throws NoSuchMethodE
getServer().getPluginManager().registerEvents(listener, this);
}
- public void downloadBridge() {
- getLogger().info("Installing StackMobBridge (utility to convert legacy mob stacks)...");
- File file = new File(getDataFolder().getParent(), "StackMobBridge.jar");
- String bridgeUrl = "http://aqua.api.spiget.org/v2/resources/45495/download";
- Utilities.downloadFile(file, bridgeUrl).whenComplete(((downloadResult, throwable) -> {
- if (downloadResult == Utilities.DownloadResult.ERROR) {
- getLogger().log(Level.SEVERE,"There was an issue while downloading StackMobBridge.");
- getLogger().log(Level.SEVERE, "This means that mob stacks will not be converted to the newer format.");
- return;
- }
- if (getServer().getPluginManager().getPlugin("StackMobBridge") != null) {
- return;
- }
- try {
- Plugin plugin = getPluginLoader().loadPlugin(file);
- getPluginLoader().enablePlugin(plugin);
- } catch (InvalidPluginException e) {
- e.printStackTrace();
- }
- }));
- }
-
public EntityTranslation getEntityTranslation() {
return entityTranslation;
}
@@ -206,10 +194,6 @@ public NamespacedKey getStackKey() {
return stackKey;
}
- public NamespacedKey getWaitKey() {
- return waitKey;
- }
-
public NamespacedKey getToolKey() {
return toolKey;
}
@@ -217,4 +201,16 @@ public NamespacedKey getToolKey() {
public ItemTools getItemTools() {
return itemTools;
}
+
+ public boolean isStepDamageError() {
+ return stepDamageError;
+ }
+
+ public void setStepDamageError(boolean stepDamageError) {
+ this.stepDamageError = stepDamageError;
+ }
+
+ public Scheduler getScheduler() {
+ return scheduler;
+ }
}
diff --git a/src/main/java/uk/antiperson/stackmob/commands/ArgumentType.java b/src/main/java/uk/antiperson/stackmob/commands/ArgumentType.java
index 746a684f..f75a8911 100644
--- a/src/main/java/uk/antiperson/stackmob/commands/ArgumentType.java
+++ b/src/main/java/uk/antiperson/stackmob/commands/ArgumentType.java
@@ -4,5 +4,6 @@ public enum ArgumentType {
BOOLEAN,
STRING,
INTEGER,
- ENTITY_TYPE
+ ENTITY_TYPE,
+ WORLD
}
diff --git a/src/main/java/uk/antiperson/stackmob/commands/CommandArgument.java b/src/main/java/uk/antiperson/stackmob/commands/CommandArgument.java
index 415daab9..f3b28133 100644
--- a/src/main/java/uk/antiperson/stackmob/commands/CommandArgument.java
+++ b/src/main/java/uk/antiperson/stackmob/commands/CommandArgument.java
@@ -1,7 +1,9 @@
package uk.antiperson.stackmob.commands;
+import org.bukkit.Bukkit;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Mob;
+import uk.antiperson.stackmob.utils.Utilities;
import java.util.ArrayList;
import java.util.Arrays;
@@ -12,10 +14,12 @@ public class CommandArgument {
private final ArgumentType type;
private final boolean optional;
private final List expectedArguments;
- public CommandArgument(ArgumentType type, boolean optional, List expectedArguments) {
+ private final String name;
+ private CommandArgument(ArgumentType type, boolean optional, List expectedArguments, String name) {
this.type = type;
this.optional = optional;
this.expectedArguments = expectedArguments;
+ this.name = name;
}
public ArgumentType getType() {
@@ -45,20 +49,40 @@ public List getExpectedArguments() {
return strings;
case BOOLEAN:
return Arrays.asList("true", "false");
+ case WORLD:
+ Bukkit.getWorlds().forEach(world -> strings.add(world.getName()));
+ return strings;
}
return strings;
}
- public static CommandArgument construct(ArgumentType type) {
- return new CommandArgument(type, false, null);
+ public String buildString() {
+ StringBuilder options = new StringBuilder();
+ if (getExpectedArguments().size() <= 3 && getExpectedArguments().size() > 0) {
+ getExpectedArguments().forEach(argument -> options.append(argument).append("/"));
+ options.deleteCharAt(options.length() - 1);
+ } else if (getName() != null) {
+ options.append(getName());
+ } else {
+ options.append(Utilities.filter(getType().toString()).toLowerCase());
+ }
+ return options.toString();
+ }
+
+ public String getName() {
+ return name;
}
public static CommandArgument construct(ArgumentType type, boolean optional) {
- return new CommandArgument(type, optional, null);
+ return new CommandArgument(type, optional, null, null);
+ }
+
+ public static CommandArgument construct(ArgumentType type, boolean optional, String name) {
+ return new CommandArgument(type, optional,null, name);
}
public static CommandArgument construct(ArgumentType type, boolean optional, List expectedArguments) {
- return new CommandArgument(type, optional, expectedArguments);
+ return new CommandArgument(type, optional, expectedArguments, null);
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/commands/Commands.java b/src/main/java/uk/antiperson/stackmob/commands/Commands.java
index cf263e4b..f331d60b 100644
--- a/src/main/java/uk/antiperson/stackmob/commands/Commands.java
+++ b/src/main/java/uk/antiperson/stackmob/commands/Commands.java
@@ -1,99 +1,106 @@
package uk.antiperson.stackmob.commands;
-import org.apache.commons.lang.ArrayUtils;
-import org.bukkit.ChatColor;
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextColor;
+import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
-import org.bukkit.entity.*;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import uk.antiperson.stackmob.StackMob;
import uk.antiperson.stackmob.commands.subcommands.*;
import uk.antiperson.stackmob.utils.Utilities;
+import java.lang.reflect.InvocationTargetException;
import java.util.*;
public class Commands implements CommandExecutor, TabCompleter {
private final StackMob sm;
- private final Set subCommands;
+ private final TreeMap subCommands;
+
public Commands(StackMob sm) {
this.sm = sm;
- this.subCommands = new HashSet<>();
+ this.subCommands = new TreeMap<>();
}
public void registerSubCommands() {
- subCommands.add(new About(sm));
- subCommands.add(new SpawnStack(sm));
- subCommands.add(new Remove(sm));
- subCommands.add(new CheckUpdate(sm));
- subCommands.add(new Upgrade(sm));
- subCommands.add(new GiveTool(sm));
- subCommands.add(new Reload(sm));
- subCommands.add(new ForceStack(sm));
+ register(About.class);
+ register(SpawnStack.class);
+ register(Remove.class);
+ register(CheckUpdate.class);
+ register(Upgrade.class);
+ register(GiveTool.class);
+ register(Reload.class);
+ register(ForceStack.class);
+ register(Stats.class);
+ }
+
+ private void register(Class extends SubCommand> subCommandClass) {
+ SubCommand subCommand;
+ try {
+ subCommand = subCommandClass.getConstructor(StackMob.class).newInstance(sm);
+ } catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
+ sm.getLogger().info("Error occurred while trying to register " + subCommandClass.getSimpleName() + " sub command!");
+ e.printStackTrace();
+ return;
+ }
+ subCommands.put(subCommand.getCommand(), subCommand);
}
@Override
- public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] strings) {
- if (!(commandSender.hasPermission("stackmob.admin"))) {
- commandSender.sendMessage(Utilities.PREFIX + ChatColor.RED + "You do not have permission!");
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String s, @NotNull String[] strings) {
+ String cmd = Bukkit.getServer().getPluginCommand("sm").getPlugin().equals(sm) ? "sm" : "stackmob";
+ if (!(sender.hasPermission("stackmob.admin"))) {
+ sendError(sender, "You do not have permission!");
return false;
}
if (strings.length == 0) {
- commandSender.sendMessage(Utilities.PREFIX + ChatColor.GOLD + "Commands: ");
- for (SubCommand subCommand : subCommands) {
- StringBuilder args = new StringBuilder();
- for (CommandArgument argumentType : subCommand.getArguments()) {
- StringBuilder options = new StringBuilder();
- if (argumentType.getExpectedArguments().size() <= 3 && argumentType.getExpectedArguments().size() > 0) {
- argumentType.getExpectedArguments().forEach(argument -> options.append(argument).append("/"));
- options.deleteCharAt(options.length() - 1);
- } else {
- options.append(argumentType.getType());
- }
- if (argumentType.isOptional()) {
- args.append("(").append(options).append(") ");
- continue;
- }
- args.append("[").append(options).append("] ");
- }
- commandSender.sendMessage(ChatColor.AQUA + "/sm " + subCommand.getCommand() + " " + args + ChatColor.GRAY + "- " + ChatColor.YELLOW + subCommand.getDescription());
+ Component commands = Utilities.PREFIX.append(Component.text("Commands:").color(TextColor.color(255, 127, 80)));
+ sender.sendMessage(commands);
+ for (SubCommand subCommand : subCommands.values()) {
+ sender.sendMessage(subCommand.buildComponent(cmd));
}
- commandSender.sendMessage(ChatColor.GOLD + "Key: () = Optional argument, [] = Mandatory argument.");
+ Component key = Component.text("Key: () = Optional argument, [] = Mandatory argument.").color(TextColor.color(255, 127, 80));
+ sender.sendMessage(key);
return false;
}
- for (SubCommand subCommand : subCommands) {
- if (!subCommand.getCommand().equalsIgnoreCase(strings[0])) {
- continue;
- }
- if (subCommand.isPlayerRequired() && !(commandSender instanceof Player)) {
- commandSender.sendMessage(Utilities.PREFIX + ChatColor.RED + "You are not a player!");
- return false;
- }
- if (!validateArgs(subCommand.getArguments(), (String[]) ArrayUtils.remove(strings, 0))) {
- commandSender.sendMessage(Utilities.PREFIX + ChatColor.RED + "Invalid arguments!");
- return false;
- }
- subCommand.onCommand(new User(commandSender), (String[]) ArrayUtils.remove(strings, 0));
+ SubCommand subCommand = subCommands.get(strings[0].toLowerCase());
+ if (subCommand == null) {
+ sendError(sender, "Invalid subcommand!");
+ return false;
}
+ if (subCommand.isPlayerRequired() && !(sender instanceof Player)) {
+ sendError(sender, "This subcommand requires a player!");
+ return false;
+ }
+ String[] subCmdArgs = Utilities.removeFirst(strings);
+ if (!validateArgs(subCommand.getArguments(), subCmdArgs)) {
+ sendError(sender, "Invalid arguments for '" + subCommand.getCommand() + "'. Usage:");
+ sender.sendMessage(subCommand.buildComponent(cmd));
+ return false;
+ }
+ subCommand.onCommand(new User(sender, sender), subCmdArgs);
return false;
}
+ private void sendError(Audience audience, String message) {
+ Component noPerm = Utilities.PREFIX.append(Component.text(message).color(NamedTextColor.RED));
+ audience.sendMessage(noPerm);
+ }
+
public boolean validateArgs(CommandArgument[] argumentTypes, String[] args) {
- if (args.length < argumentTypes.length) {
- if (argumentTypes.length == (args.length + 1)) {
- CommandArgument argument = argumentTypes[argumentTypes.length - 1];
- return argument.isOptional();
- }
- return false;
- }
- for (int i = 0; i < argumentTypes.length; i++) {
+ for (int i = 0; i < args.length; i++) {
CommandArgument argument = argumentTypes[i];
switch (argument.getType()) {
case BOOLEAN:
- if (!(args[i].equals("true") || args[i+1].equals("false"))) return false;
+ if (!(args[i].equals("true") || args[i].equals("false"))) return false;
break;
case INTEGER:
try {
@@ -104,50 +111,56 @@ public boolean validateArgs(CommandArgument[] argumentTypes, String[] args) {
break;
case ENTITY_TYPE:
try {
- EntityType.valueOf(args[i]);
+ EntityType.valueOf(args[i].toUpperCase());
} catch (IllegalArgumentException e) {
return false;
}
break;
+ case WORLD:
+ if (Bukkit.getWorld(args[i]) == null) {
+ return false;
+ }
+ break;
case STRING:
if (!argument.getExpectedArguments().contains(args[i])) {
return false;
}
}
}
+ if (args.length < argumentTypes.length) {
+ if (argumentTypes.length == (args.length + 1)) {
+ CommandArgument argument = argumentTypes[argumentTypes.length - 1];
+ return argument.isOptional();
+ }
+ return false;
+ }
return true;
}
@Nullable
@Override
public List onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] strings) {
- if (strings.length == 1) {
- List args = new ArrayList<>();
- for (SubCommand subCommand : subCommands) {
- String commandString = subCommand.getCommand();
+ Set expectedArguments = getExpectedArguments(strings);
+ expectedArguments.removeIf(possibleArgument -> !possibleArgument.toLowerCase().startsWith(strings[strings.length - 1].toLowerCase()));
+ return new ArrayList<>(expectedArguments);
+ }
- if (!commandString.toLowerCase().startsWith(strings[0].toLowerCase())) {
- continue;
- }
- args.add(subCommand.getCommand());
+ private Set getExpectedArguments(String[] strings) {
+ SubCommand subCommand = subCommands.get(strings[0].toLowerCase());
+ if (subCommand == null) {
+ if (strings.length > 1) {
+ return Collections.emptySet();
}
- return args;
+ return new HashSet<>(subCommands.keySet());
}
- for (SubCommand subCommand : subCommands) {
- if (!subCommand.getCommand().equalsIgnoreCase(strings[0])) {
- continue;
- }
- if (subCommand.getArguments().length < strings.length - 1) {
- return null;
- }
- CommandArgument commandArgument = subCommand.getArguments()[strings.length - 2];
-
- List expectedArguments = new LinkedList<>(commandArgument.getExpectedArguments());
- expectedArguments.removeIf(possibleArgument -> !possibleArgument.toLowerCase().startsWith(strings[strings.length - 1].toLowerCase()));
- Collections.sort(expectedArguments);
-
- return expectedArguments;
+ // the subcommand is correct, so length of arguments is length of string array - 1
+ int subCmdArgsLength = strings.length - 1;
+ // if the command has no arguments or the arguments exceed cmd length
+ if (subCmdArgsLength == 0 || subCmdArgsLength > subCommand.getArguments().length) {
+ return Collections.emptySet();
}
- return null;
+ // get the argument at this position in the cmd
+ CommandArgument commandArgument = subCommand.getArguments()[subCmdArgsLength - 1];
+ return new TreeSet<>(commandArgument.getExpectedArguments());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/commands/SubCommand.java b/src/main/java/uk/antiperson/stackmob/commands/SubCommand.java
index 2bb7ae14..4a88db22 100644
--- a/src/main/java/uk/antiperson/stackmob/commands/SubCommand.java
+++ b/src/main/java/uk/antiperson/stackmob/commands/SubCommand.java
@@ -1,5 +1,9 @@
package uk.antiperson.stackmob.commands;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextColor;
+
public abstract class SubCommand implements Command {
private final CommandArgument[] arguments;
@@ -27,4 +31,20 @@ private CommandMetadata getCommandMetadata() {
return getClass().getAnnotation(CommandMetadata.class);
}
+ public Component buildComponent(String cmd) {
+ StringBuilder args = new StringBuilder();
+ for (CommandArgument argumentType : getArguments()) {
+ String options = argumentType.buildString();
+ if (argumentType.isOptional()) {
+ args.append("(").append(options).append(") ");
+ continue;
+ }
+ args.append("[").append(options).append("] ");
+ }
+ Component label = Component.text("/" + cmd + " " + getCommand() + " " + args).color(TextColor.color(60, 179, 113));
+ Component separator = Component.text("- ").color(NamedTextColor.GRAY);
+ Component desc = Component.text(getDescription()).color(TextColor.color(144, 238, 144));
+ return label.append(separator).append(desc);
+ }
+
}
diff --git a/src/main/java/uk/antiperson/stackmob/commands/User.java b/src/main/java/uk/antiperson/stackmob/commands/User.java
index 364bf6f4..34cdc11d 100644
--- a/src/main/java/uk/antiperson/stackmob/commands/User.java
+++ b/src/main/java/uk/antiperson/stackmob/commands/User.java
@@ -1,18 +1,23 @@
package uk.antiperson.stackmob.commands;
-import org.bukkit.ChatColor;
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
import uk.antiperson.stackmob.utils.Utilities;
public class User {
- private final CommandSender sender;
- public User(CommandSender sender) {
+ private final Audience sender;
+ private final CommandSender cmdSender;
+
+ public User(Audience sender, CommandSender cmdSender) {
this.sender = sender;
+ this.cmdSender = cmdSender;
}
public void sendRawMessage(String message) {
- sender.sendMessage(message);
+ sender.sendMessage(Component.text(message));
}
public void sendInfo(String message) {
@@ -28,24 +33,26 @@ public void sendSuccess(String message) {
}
public CommandSender getSender() {
- return sender;
+ return cmdSender;
+ }
+
+ private void sendMessage(MessageType type, String string) {
+ sendMessage(type, Component.text(string));
}
- private void sendMessage(MessageType type, String rawMessage) {
- StringBuilder message = new StringBuilder(Utilities.PREFIX);
+ private void sendMessage(MessageType type, Component component) {
switch (type) {
case INFO:
- message.append(ChatColor.YELLOW);
+ component = component.color(NamedTextColor.YELLOW);
break;
case ERROR:
- message.append(ChatColor.RED);
+ component = component.color(NamedTextColor.RED);
break;
case SUCCESS:
- message.append(ChatColor.GREEN);
+ component = component.color(NamedTextColor.GREEN);
break;
}
- message.append(rawMessage);
- sender.sendMessage(message.toString());
+ sender.sendMessage(Utilities.PREFIX.append(component));
}
enum MessageType {
diff --git a/src/main/java/uk/antiperson/stackmob/commands/subcommands/CheckUpdate.java b/src/main/java/uk/antiperson/stackmob/commands/subcommands/CheckUpdate.java
index ae65e133..38a30668 100644
--- a/src/main/java/uk/antiperson/stackmob/commands/subcommands/CheckUpdate.java
+++ b/src/main/java/uk/antiperson/stackmob/commands/subcommands/CheckUpdate.java
@@ -15,7 +15,7 @@ public CheckUpdate(StackMob sm) {
@Override
public boolean onCommand(User sender, String[] args) {
- sender.sendInfo("Contacting SpigotMC. Please wait...");
+ sender.sendInfo("Contacting Modrinth. Please wait...");
sm.getUpdater().checkUpdate().whenComplete((updateResult, throwable) -> {
switch (updateResult.getResult()) {
case ERROR:
diff --git a/src/main/java/uk/antiperson/stackmob/commands/subcommands/ForceStack.java b/src/main/java/uk/antiperson/stackmob/commands/subcommands/ForceStack.java
index 2501ce2b..7b2064a6 100644
--- a/src/main/java/uk/antiperson/stackmob/commands/subcommands/ForceStack.java
+++ b/src/main/java/uk/antiperson/stackmob/commands/subcommands/ForceStack.java
@@ -4,6 +4,7 @@
import org.bukkit.World;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Mob;
+import org.bukkit.entity.Player;
import org.bukkit.entity.Tameable;
import org.bukkit.event.entity.CreatureSpawnEvent;
import uk.antiperson.stackmob.StackMob;
@@ -11,41 +12,55 @@
import uk.antiperson.stackmob.utils.Utilities;
import java.util.Arrays;
+import java.util.function.Predicate;
-@CommandMetadata(command = "forcestack", playerReq = false, desc = "Force all currently loaded entities to stack")
+@CommandMetadata(command = "forcestack", playerReq = false, desc = "Force all currently loaded entities to stack.")
public class ForceStack extends SubCommand {
private final StackMob sm;
public ForceStack(StackMob sm) {
- super(CommandArgument.construct(ArgumentType.STRING, true, Arrays.asList("named", "tamed")));
+ super(CommandArgument.construct(ArgumentType.STRING, true, Arrays.asList("named", "tamed", "chunk")));
this.sm = sm;
}
@Override
public boolean onCommand(User sender, String[] args) {
int count = 0;
-
+ Predicate predicate = null;
+ if (args.length > 0) {
+ switch (args[0].toLowerCase()) {
+ case "named":
+ predicate = pEntity -> pEntity.getCustomName() != null;
+ break;
+ case "tamed":
+ predicate = pEntity -> (pEntity instanceof Tameable) && ((Tameable) pEntity).isTamed();
+ break;
+ case "chunk":
+ if (!(sender.getSender() instanceof Player)) {
+ sender.sendError("You need to be a player!");
+ return false;
+ }
+ predicate = pEntity -> pEntity.getLocation().getChunk() == ((Player) sender.getSender()).getLocation().getChunk();
+ break;
+ }
+ }
for (World world : Bukkit.getWorlds()) {
for (LivingEntity entity : world.getEntitiesByClass(Mob.class)) {
if (sm.getEntityManager().isStackedEntity(entity)) {
continue;
}
- if (entity.getCustomName() != null && !(args.length > 0 && args[0].equals("named"))) {
+ if (predicate != null && !predicate.test(entity)) {
continue;
}
- if ((entity instanceof Tameable) && (((Tameable) entity)).isTamed() && !(args.length > 0 && args[0].equals("tamed"))) {
+ if (sm.getMainConfig().getConfig(entity).isEntityBlacklisted(entity)) {
continue;
}
- CreatureSpawnEvent.SpawnReason reason = Utilities.isPaper() ? entity.getEntitySpawnReason() : null;
- if (sm.getMainConfig().isEntityBlacklisted(entity, reason)) {
- continue;
- }
- sm.getEntityManager().getStackEntity(entity).setSize(1);
+ sm.getEntityManager().registerStackedEntity(entity).setSize(1);
count++;
}
}
-
- sender.sendSuccess(count + " entities has been forced to stack!");
+ String entityType = predicate != null ? args[0].toLowerCase() + " " : "";
+ sender.sendSuccess(count + " " + entityType + "entities have been forced to stack!");
return false;
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/commands/subcommands/Reload.java b/src/main/java/uk/antiperson/stackmob/commands/subcommands/Reload.java
index 8d01ab74..c76e5976 100644
--- a/src/main/java/uk/antiperson/stackmob/commands/subcommands/Reload.java
+++ b/src/main/java/uk/antiperson/stackmob/commands/subcommands/Reload.java
@@ -18,8 +18,8 @@ public Reload(StackMob sm) {
@Override
public boolean onCommand(User sender, String[] args) {
try {
- sm.getEntityTranslation().load();
- sm.getMainConfig().load();
+ sm.getEntityTranslation().reloadConfig();
+ sm.getMainConfig().reload();
sender.sendSuccess("Reloaded config files successfully!");
sender.sendInfo("Note: Some config changes may require a full server restart to take effect.");
} catch (IOException e) {
diff --git a/src/main/java/uk/antiperson/stackmob/commands/subcommands/Remove.java b/src/main/java/uk/antiperson/stackmob/commands/subcommands/Remove.java
index f0235933..6e23bf38 100644
--- a/src/main/java/uk/antiperson/stackmob/commands/subcommands/Remove.java
+++ b/src/main/java/uk/antiperson/stackmob/commands/subcommands/Remove.java
@@ -1,28 +1,39 @@
package uk.antiperson.stackmob.commands.subcommands;
-import org.bukkit.entity.*;
+import org.bukkit.Bukkit;
+import org.bukkit.Chunk;
+import org.bukkit.entity.Animals;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.entity.Mob;
+import org.bukkit.entity.Monster;
+import org.bukkit.entity.Player;
import uk.antiperson.stackmob.StackMob;
import uk.antiperson.stackmob.commands.*;
+import uk.antiperson.stackmob.entity.StackEntity;
+import uk.antiperson.stackmob.utils.Utilities;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.function.Function;
-@CommandMetadata(command = "remove", playerReq = true, desc = "Remove all entities")
+@CommandMetadata(command = "remove", playerReq = false, desc = "Remove entities.")
public class Remove extends SubCommand {
private final StackMob sm;
public Remove(StackMob sm) {
- super(CommandArgument.construct(ArgumentType.STRING, true, Arrays.asList("animals", "hostile")));
+ super(CommandArgument.construct(ArgumentType.STRING, false, Arrays.asList("chunk", "world", "all")),
+ CommandArgument.construct(ArgumentType.STRING, true, Arrays.asList("animals", "hostile")));
this.sm = sm;
}
@Override
public boolean onCommand(User sender, String[] args) {
- Player player = (Player) sender.getSender();
Function function = entity -> entity instanceof Mob;
- if (args.length == 1) {
- switch (args[0]) {
+ if (args.length == 2) {
+ switch (args[1]) {
case "animals":
function = entity -> entity instanceof Animals;
break;
@@ -31,17 +42,39 @@ public boolean onCommand(User sender, String[] args) {
break;
}
}
- List entities = player.getWorld().getLivingEntities();
- for (LivingEntity entity : entities) {
- if (!function.apply(entity)) {
- continue;
- }
- if (!sm.getEntityManager().isStackedEntity(entity)) {
- continue;
+ Set chunks = new HashSet<>();
+ switch (args[0]) {
+ case "chunk":
+ if (!(sender.getSender() instanceof Player)) {
+ sender.sendError("You need to be a player!");
+ return false;
+ }
+ chunks.add(((Player) sender.getSender()).getLocation().getChunk());
+ break;
+ case "world":
+ if (!(sender.getSender() instanceof Player)) {
+ sender.sendError("You need to be a player!");
+ return false;
+ }
+ chunks.addAll(List.of(((Player) sender.getSender()).getWorld().getLoadedChunks()));
+ break;
+ case "all":
+ Bukkit.getWorlds().forEach(world -> chunks.addAll(List.of(world.getLoadedChunks())));
+ break;
+ }
+ for (Chunk chunk : chunks) {
+ for (Entity entity : chunk.getEntities()) {
+ if (!function.apply(entity)) {
+ continue;
+ }
+ StackEntity stackEntity = sm.getEntityManager().getStackEntity((LivingEntity) entity);
+ if (stackEntity == null) {
+ continue;
+ }
+ stackEntity.remove();
}
- entity.remove();
}
- sender.sendSuccess("Entities matching your criteria have been removed.");
+ sender.sendSuccess(Utilities.capitalizeString(args[0]) + " entities matching your criteria have been removed.");
return false;
}
diff --git a/src/main/java/uk/antiperson/stackmob/commands/subcommands/SpawnStack.java b/src/main/java/uk/antiperson/stackmob/commands/subcommands/SpawnStack.java
index 46ee34a8..41722b46 100644
--- a/src/main/java/uk/antiperson/stackmob/commands/subcommands/SpawnStack.java
+++ b/src/main/java/uk/antiperson/stackmob/commands/subcommands/SpawnStack.java
@@ -1,6 +1,5 @@
package uk.antiperson.stackmob.commands.subcommands;
-import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
@@ -8,27 +7,55 @@
import uk.antiperson.stackmob.commands.*;
import uk.antiperson.stackmob.entity.StackEntity;
+import java.util.HashSet;
+import java.util.UUID;
+
@CommandMetadata(command = "spawn", playerReq = true, desc = "Spawn a new stack.")
public class SpawnStack extends SubCommand {
private final StackMob sm;
+ private final HashSet confirm;
public SpawnStack(StackMob sm) {
- super(CommandArgument.construct(ArgumentType.ENTITY_TYPE), CommandArgument.construct(ArgumentType.INTEGER));
+ super(CommandArgument.construct(ArgumentType.ENTITY_TYPE, false),
+ CommandArgument.construct(ArgumentType.INTEGER, false, "stack size"),
+ CommandArgument.construct(ArgumentType.INTEGER, true, "number of stacks"));
this.sm = sm;
+ this.confirm = new HashSet<>();
}
@Override
public boolean onCommand(User sender, String[] args) {
Player player = (Player) sender.getSender();
- Entity entity = player.getWorld().spawnEntity(player.getLocation(), EntityType.valueOf(args[0].toUpperCase()));
- StackEntity stackEntity = sm.getEntityManager().getStackEntity((LivingEntity) entity);
+ EntityType entityType = EntityType.valueOf(args[0].toUpperCase());
int newSize = Integer.parseInt(args[1]);
- if (newSize > stackEntity.getMaxSize()) {
- sender.sendError("New stack value is too large! (max = " + stackEntity.getMaxSize() + ")");
+ if (newSize < 1) {
+ sender.sendError("You cannot spawn a stack with size less than one!");
+ return false;
+ }
+ int maxSize = sm.getMainConfig().getConfig(entityType).getMaxStack();
+ if (newSize > maxSize) {
+ sender.sendError("Provided stack value is too large! (the maximum for " + entityType + " is " + maxSize + ")");
+ return false;
+ }
+ int amountOfStacks = args.length > 2 ? Integer.parseInt(args[2]) : 1;
+ if (amountOfStacks < 1) {
+ sender.sendError("You cannot spawn less than one stack!");
+ return false;
+ }
+ if (amountOfStacks > 20 && !confirm.contains(((Player) sender.getSender()).getUniqueId())) {
+ sender.sendInfo("Are you sure you want to spawn " + amountOfStacks + " stacks?");
+ sender.sendInfo("Run the same command again to confirm.");
+ confirm.add(((Player) sender.getSender()).getUniqueId());
return false;
}
- stackEntity.setSize(newSize);
- sender.sendSuccess("A new stack has been spawned.");
+ for (int i = 0; i < amountOfStacks; i++) {
+ LivingEntity entity = (LivingEntity) player.getWorld().spawnEntity(player.getLocation(), entityType);
+ StackEntity stackEntity = sm.getEntityManager().registerStackedEntity(entity);
+ stackEntity.setSize(newSize);
+ }
+ String stackString = amountOfStacks == 1 ? "A new stack has" : amountOfStacks + " stacks have";
+ sender.sendSuccess(stackString + " been spawned.");
+ confirm.remove(((Player) sender.getSender()).getUniqueId());
return false;
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/commands/subcommands/Stats.java b/src/main/java/uk/antiperson/stackmob/commands/subcommands/Stats.java
new file mode 100644
index 00000000..3b2a68c7
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/commands/subcommands/Stats.java
@@ -0,0 +1,40 @@
+package uk.antiperson.stackmob.commands.subcommands;
+
+import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.commands.CommandMetadata;
+import uk.antiperson.stackmob.commands.SubCommand;
+import uk.antiperson.stackmob.commands.User;
+import uk.antiperson.stackmob.entity.StackEntity;
+
+@CommandMetadata(command = "stats", playerReq = false, desc = "View plugin statistics.")
+public class Stats extends SubCommand {
+
+ private final StackMob sm;
+ public Stats(StackMob sm) {
+ this.sm = sm;
+ }
+
+ @Override
+ public boolean onCommand(User sender, String[] args) {
+ sendMobStats(sender);
+ return false;
+ }
+
+ private void sendMobStats(User sender) {
+ int total = 0;
+ int waiting = 0;
+ int full = 0;
+ for (StackEntity stackEntity : sm.getEntityManager().getStackEntities()) {
+ if (stackEntity.isWaiting()) {
+ waiting += 1;
+ }
+ if (stackEntity.isMaxSize()) {
+ full += 1;
+ }
+ total += stackEntity.getSize();
+ }
+ sender.sendInfo("Stacking statistics:");
+ sender.sendRawMessage("Total stack entities: " + sm.getEntityManager().getStackEntities().size() + " (" + total + " single entities.)");
+ sender.sendRawMessage("Full stacks: " + full + " Waiting to stack: " + waiting);
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/commands/subcommands/Upgrade.java b/src/main/java/uk/antiperson/stackmob/commands/subcommands/Upgrade.java
index d412afa4..69326994 100644
--- a/src/main/java/uk/antiperson/stackmob/commands/subcommands/Upgrade.java
+++ b/src/main/java/uk/antiperson/stackmob/commands/subcommands/Upgrade.java
@@ -19,7 +19,7 @@ public boolean onCommand(User sender, String[] args) {
sm.getUpdater().downloadUpdate().whenComplete((downloadResult, throwable) -> {
switch (downloadResult) {
case ERROR:
- sender.sendError("An error occurred while checking for updates.");
+ sender.sendError("An error occurred while downloading the update.");
break;
case SUCCESSFUL:
sender.sendSuccess("The new update was downloaded successfully!");
diff --git a/src/main/java/uk/antiperson/stackmob/config/Config.java b/src/main/java/uk/antiperson/stackmob/config/Config.java
new file mode 100644
index 00000000..ed5c16f7
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/config/Config.java
@@ -0,0 +1,10 @@
+package uk.antiperson.stackmob.config;
+
+import java.io.IOException;
+
+public interface Config {
+
+ void load() throws IOException;
+
+ void reloadConfig() throws IOException;
+}
diff --git a/src/main/java/uk/antiperson/stackmob/config/ConfigFile.java b/src/main/java/uk/antiperson/stackmob/config/ConfigFile.java
index c949a489..af421084 100644
--- a/src/main/java/uk/antiperson/stackmob/config/ConfigFile.java
+++ b/src/main/java/uk/antiperson/stackmob/config/ConfigFile.java
@@ -14,8 +14,9 @@
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.List;
+import java.util.Set;
-public class ConfigFile {
+public abstract class ConfigFile implements Config {
private File file;
private FileConfiguration fileCon;
@@ -47,7 +48,12 @@ public boolean getBoolean(String path) {
}
public ConfigList getList(String path) {
- return ConfigList.getConfigList(this, new ConfigValue(path, get(path)));
+ List> list = fileCon.getList(path);
+ if (list == null) {
+ throw new UnsupportedOperationException(path + " list is null!");
+ }
+ boolean inverted = getBoolean(path + "-invert");
+ return new ConfigList(this, list, path, inverted);
}
public ConfigurationSection getConfigurationSection(String path) {
@@ -78,6 +84,10 @@ public void set(String path, Object value) {
fileCon.set(path, value);
}
+ public Set getKeys(boolean deep) {
+ return fileCon.getKeys(deep);
+ }
+
/**
* Loads the config so that it can be read.
* @throws IOException when an I/O error occurs if a new file is made.
@@ -109,7 +119,7 @@ public void createFile() throws IOException {
// The files copy below will throw an error if the directories are not pre existing.
file = new File(sm.getDataFolder(), filePath);
File parentFile = file.getParentFile();
- if(!parentFile.exists()){
+ if (!parentFile.exists()) {
Files.createDirectories(parentFile.toPath());
}
// Open the file and copy it to the plugin folder.
@@ -140,15 +150,20 @@ public void updateFile() throws IOException {
continue;
}
fileCon.set(key, includedConfig.get(key));
+ if (Utilities.isVersionAtLeast(Utilities.MinecraftVersion.V1_18_2)) {
+ fileCon.setComments(key, includedConfig.getComments(key));
+ }
updated = true;
}
if (!updated) {
return;
}
fileCon.options().header(includedConfig.options().header());
- sm.getLogger().info("Config file " + file.getName() + " has been updated.");
- sm.getLogger().info("Unfortunately, this means that comments have been removed.");
- sm.getLogger().info("If you need comments, you access a version with them at " + Utilities.GITHUB_DEFAULT_CONFIG);
+ sm.getLogger().info("Config file " + file.getName() + " has been updated with new values.");
+ if (!Utilities.isVersionAtLeast(Utilities.MinecraftVersion.V1_18_2)) {
+ sm.getLogger().warning("This means that comments have been removed.");
+ sm.getLogger().info("If you need comments, you access a version with them at " + Utilities.GITHUB_DEFAULT_CONFIG);
+ }
save();
}
diff --git a/src/main/java/uk/antiperson/stackmob/config/ConfigList.java b/src/main/java/uk/antiperson/stackmob/config/ConfigList.java
index 3643ea55..404b0f51 100644
--- a/src/main/java/uk/antiperson/stackmob/config/ConfigList.java
+++ b/src/main/java/uk/antiperson/stackmob/config/ConfigList.java
@@ -8,6 +8,7 @@ public class ConfigList {
private final String path;
private final boolean inverted;
private final ConfigFile configFile;
+
public ConfigList(ConfigFile configFile, List> list, String path, boolean inverted) {
this.configFile = configFile;
this.list = list;
@@ -17,28 +18,26 @@ public ConfigList(ConfigFile configFile, List> list, String path, boolean inve
/**
* List contains method which supports inverting lists.
- * @param tocheck object to check is in the list
+ * @param item object to check is in the list
* @return whether this object is in the list.
*/
- public boolean contains(Object tocheck) {
+ public boolean contains(String item) {
if (inverted){
- return !list.contains(tocheck);
+ return !rawContains(item);
}
- return list.contains(tocheck);
+ return rawContains(item);
}
- public List asIntList() {
- return configFile.getIntegerList(path);
+ public boolean isInverted() {
+ return inverted;
}
- public static ConfigList getConfigList(ConfigFile configFile, ConfigValue value) {
- List> list = (List>) value.getValue();
- String path = value.getPath();
- if (list == null) {
- throw new UnsupportedOperationException(path + " list is null!");
- }
- boolean inverted = configFile.getBoolean(value.getPath() + "-invert");
- return new ConfigList(configFile, list, path, inverted);
+ public boolean rawContains(String item) {
+ return list.contains(item);
+ }
+
+ public List asIntList() {
+ return configFile.getIntegerList(path);
}
diff --git a/src/main/java/uk/antiperson/stackmob/config/ConfigValue.java b/src/main/java/uk/antiperson/stackmob/config/ConfigValue.java
index 11aea020..f3669c86 100644
--- a/src/main/java/uk/antiperson/stackmob/config/ConfigValue.java
+++ b/src/main/java/uk/antiperson/stackmob/config/ConfigValue.java
@@ -1,10 +1,17 @@
package uk.antiperson.stackmob.config;
+import uk.antiperson.stackmob.utils.Utilities;
+
+import java.util.Collections;
+import java.util.List;
+
public class ConfigValue {
private final String path;
private final Object value;
- public ConfigValue(String path, Object value) {
+ private final ConfigFile configFile;
+ public ConfigValue(ConfigFile configFile, String path, Object value) {
+ this.configFile = configFile;
this.path = path;
this.value = value;
}
@@ -16,4 +23,33 @@ public String getPath() {
public Object getValue() {
return value;
}
+
+ public ConfigList getList() {
+ Object obj = getValue() instanceof List> ? getValue() : Collections.emptyList();
+ boolean inverted = configFile.getBoolean(getPath() + "-invert");
+ return new ConfigList(configFile, (List>) obj, getPath(), inverted);
+ }
+
+ public boolean getBoolean() {
+ return getValue() instanceof Boolean ? (Boolean) getValue() : false;
+ }
+
+ public double getDouble() {
+ if (getValue() instanceof Integer) {
+ return Utilities.toInt(getValue().toString());
+ }
+ return getValue() instanceof Double ? Utilities.toDouble(getValue().toString()) : 0;
+ }
+
+ public int getInt() {
+ return getValue() instanceof Number ? Utilities.toInt(getValue().toString()) : 0;
+ }
+
+ public String getString() {
+ return getValue() == null ? null : getValue().toString();
+ }
+
+ public List asIntList() {
+ return configFile == null ? Collections.emptyList() : getList().asIntList();
+ }
}
diff --git a/src/main/java/uk/antiperson/stackmob/config/EntityConfig.java b/src/main/java/uk/antiperson/stackmob/config/EntityConfig.java
new file mode 100644
index 00000000..d8dadb8c
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/config/EntityConfig.java
@@ -0,0 +1,411 @@
+package uk.antiperson.stackmob.config;
+
+import org.bukkit.World;
+import org.bukkit.entity.Animals;
+import org.bukkit.entity.Boss;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Ghast;
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.entity.Monster;
+import org.bukkit.entity.Phantom;
+import org.bukkit.entity.Raider;
+import org.bukkit.entity.WaterMob;
+import org.bukkit.event.entity.CreatureSpawnEvent;
+import org.bukkit.event.entity.EntityDamageEvent;
+import org.bukkit.event.entity.EntityKnockbackEvent;
+import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.entity.StackEntity;
+import uk.antiperson.stackmob.entity.death.DeathType;
+import uk.antiperson.stackmob.hook.hooks.JobsHook;
+import uk.antiperson.stackmob.utils.Utilities;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+public class EntityConfig {
+
+ private final Map valueMap;
+ private final EntityType type;
+ private final StackMob sm;
+
+ public EntityConfig(StackMob sm, EntityType type) {
+ this.sm = sm;
+ this.type = type;
+ this.valueMap = new HashMap<>();
+ }
+
+ public void put(String path, ConfigValue value) {
+ valueMap.put(path, value);
+ }
+
+ public ConfigValue getConfigValue(String path) {
+ ConfigValue configValue = valueMap.get(path);
+ if (configValue == null) {
+ sm.getLogger().info("The value at config path " + path + " is null. Make sure your config is correctly formatted at this path.");
+ }
+ return configValue;
+ }
+
+ public boolean getBoolean(String path) {
+ ConfigValue configValue = getConfigValue(path);
+ return configValue != null && configValue.getBoolean();
+ }
+
+ public int getInt(String path) {
+ ConfigValue configValue = getConfigValue(path);
+ return configValue == null ? 0 : configValue.getInt();
+ }
+
+ public double getDouble(String path) {
+ ConfigValue configValue = getConfigValue(path);
+ return configValue == null ? 0 : configValue.getDouble();
+ }
+
+ public String getString(String path) {
+ ConfigValue configValue = getConfigValue(path);
+ return configValue == null ? "" : configValue.getString();
+ }
+
+ public List getIntList(String path) {
+ ConfigValue configValue = getConfigValue(path);
+ return configValue == null ? Collections.emptyList() : configValue.asIntList();
+ }
+
+ public ConfigList getList(String path) {
+ ConfigValue configValue = getConfigValue(path);
+ return configValue == null ? new ConfigList(null, Collections.emptyList(), path, false) : configValue.getList();
+ }
+
+ public int getMaxStack() {
+ return getInt("stack.max-size");
+ }
+
+ public boolean getStackThresholdEnabled() {
+ return getBoolean( "stack.threshold.enabled");
+ }
+
+ public int getStackThreshold() {
+ return getInt( "stack.threshold.amount");
+ }
+
+ public Integer[] getStackRadius() {
+ return getIntList("stack.merge-range").toArray(new Integer[2]);
+ }
+
+ public int getStackInterval() {
+ return getInt("stack.interval");
+ }
+
+ public boolean isCheckHasMoved() {
+ return getBoolean("stack.check-location.enabled");
+ }
+
+ public double getCheckHasMovedDistance() {
+ return getDouble("stack.check-location.distance");
+ }
+
+ public int getCheckHasMovedTimeout() {
+ return getInt("stack.check-location.timeout");
+ }
+
+ public String getTagFormat() {
+ return getString("display-name.format");
+ }
+
+ public int getTagThreshold() {
+ return getInt( "display-name.threshold");
+ }
+
+ public StackEntity.TagMode getTagMode() {
+ return StackEntity.TagMode.valueOf(getString("display-name.visibility"));
+ }
+
+ public float getTagNearbyRadius() {
+ return getInt("display-name.nearby.radius");
+ }
+
+ public int getTagNearbyInterval() {
+ return getInt("display-name.nearby.interval");
+ }
+
+ public boolean isTagNearbyRayTrace() {
+ return getBoolean("display-name.nearby.ray-trace");
+ }
+
+ public boolean isUseArmorStand() {
+ return getBoolean("display-name.nearby.armorstand.enabled");
+ }
+
+ public double getArmorstandOffset() {
+ return getDouble("display-name.nearby.armorstand.offset");
+ }
+
+ public boolean isTraitEnabled(String traitKey) {
+ return getBoolean("traits." + traitKey);
+ }
+
+ public boolean isHookEnabled(String traitKey) {
+ return getBoolean("hooks." + traitKey);
+ }
+
+ public JobsHook.JobHookMode getJobHookMode() {
+ return JobsHook.JobHookMode.valueOf(getString("hooks.jobs.mode"));
+ }
+
+ public boolean isDropMultiEnabled() {
+ return getBoolean( "drops.enabled");
+ }
+
+ public boolean isDropLootTables() {
+ return getBoolean( "drops.use-loot-tables");
+ }
+
+ public boolean isSlimeMultiEnabled() {
+ return getBoolean( "events.multiply.slime-split");
+ }
+
+ public boolean isDropTypeBlacklist() {
+ return isEntityTypeInList("drops.type-blacklist");
+ }
+
+ public boolean isDropReasonBlacklist(EntityDamageEvent.DamageCause damageCause) {
+ return getList("drops.reason-blacklist").contains(damageCause.toString());
+ }
+
+ public ConfigList getDropItemBlacklist() {
+ return getList("drops.item-blacklist");
+ }
+
+ public ConfigList getDropItemOnePer() {
+ return getList("drops.one-per-stack");
+ }
+
+ public boolean isExpMultiEnabled() {
+ return getBoolean( "experience.enabled");
+ }
+
+ public boolean isExpTypeBlacklist() {
+ return getList("experience.type-blacklist").contains(type.toString());
+ }
+
+ public double getExpMinBound() {
+ return getDouble("experience.multiplier-min");
+ }
+
+ public double getExpMaxBound() {
+ return getDouble("experience.multiplier-max");
+ }
+
+ public boolean isPlayerStatMulti() {
+ return getBoolean( "player-stats");
+ }
+
+ public boolean isWaitingEnabled() {
+ return getBoolean( "wait-to-stack.enabled");
+ }
+
+ public boolean isWaitingTypes() {
+ return isEntityTypeInList("wait-to-stack.types-whitelist");
+ }
+
+ public boolean isWaitingReasons(CreatureSpawnEvent.SpawnReason spawnReason) {
+ return getList("wait-to-stack.reasons-whitelist").contains(spawnReason.toString());
+ }
+
+ public int getWaitingTime() {
+ return getInt("wait-to-stack.wait-time");
+ }
+
+ public int getMaxDeathStep() {
+ return getInt( "death.STEP.max-step");
+ }
+
+ public int getMinDeathStep() {
+ return getInt( "death.STEP.min-step");
+ }
+
+ public boolean isTargetingDisabledTypes() {
+ return isEntityTypeInList("disable-targeting.type-blacklist");
+ }
+
+ public boolean isTargetingDisabledReasons(CreatureSpawnEvent.SpawnReason spawnReason) {
+ return getList("disable-targeting.reason-blacklist").contains(spawnReason.toString());
+ }
+
+ public boolean isKnockbackDisabledTypes() {
+ return isEntityTypeInList("disable-knockback.type-blacklist");
+ }
+
+ public boolean isKnockbackDisabledReasons(CreatureSpawnEvent.SpawnReason spawnReason) {
+ return getList("disable-knockback.reason-blacklist").contains(spawnReason.toString());
+ }
+
+ public boolean isKnockbackDisabledCause(EntityKnockbackEvent.KnockbackCause cause) {
+ return getList("disable-knockback.cause-blacklist").contains(cause.toString());
+ }
+
+ public ListenerMode getListenerMode(EventType eventType) {
+ return ListenerMode.valueOf(getString("events." + eventType.getConfigKey() + ".mode"));
+ }
+
+ public int getEventMultiplyLimit(EventType eventType, int stackSize) {
+ int limit = getInt( "events." + eventType.getConfigKey() + ".limit");
+ return limit == -1 ? stackSize : Math.min(stackSize, limit);
+ }
+
+ public boolean isWorldBlacklisted(World world) {
+ return getList("worlds-blacklist").contains(world.getName());
+ }
+
+ public boolean isEntityBlacklisted(LivingEntity entity) {
+ CreatureSpawnEvent.SpawnReason reason = Utilities.isPaper() ? entity.getEntitySpawnReason() : CreatureSpawnEvent.SpawnReason.DEFAULT;
+ return isEntityBlacklisted(entity, reason);
+ }
+
+ public boolean isEntityBlacklisted(LivingEntity entity, CreatureSpawnEvent.SpawnReason reason) {
+ if (isEntityTypeInList("types-blacklist")) {
+ return true;
+ }
+ if (getList("reason-blacklist").contains(reason.toString())) {
+ return true;
+ }
+ return isWorldBlacklisted(entity.getWorld());
+ }
+
+ public DeathType getDeathType(LivingEntity dead) {
+ for (DeathType type : getDeathSection()) {
+ ConfigList reasons = getList("death." + type + ".reason-blacklist");
+ if (dead.getLastDamageCause() != null && reasons.contains(dead.getLastDamageCause().getCause().toString())) {
+ continue;
+ }
+ ConfigList spawnReasons = getList("death." + type + ".spawn-reason-blacklist");
+ if (Utilities.isPaper() && spawnReasons.contains(dead.getEntitySpawnReason().toString())) {
+ continue;
+ }
+ if (isEntityTypeInList("death." + type + ".type-blacklist")) {
+ continue;
+ }
+ return type;
+ }
+ throw new UnsupportedOperationException("Configuration error - unable to determine death type!");
+ }
+
+ private Collection getDeathSection() {
+ TreeMap array = new TreeMap<>();
+ for (DeathType type : DeathType.values()) {
+ array.put(getInt("death." + type + ".priority"), type);
+ }
+ return array.values();
+ }
+
+ public boolean isSkipDeathAnimation() {
+ return getBoolean( "death.skip-animation") && Utilities.isPaper();
+ }
+
+ public StackEntity.EquipItemMode getEquipItemMode() {
+ return StackEntity.EquipItemMode.valueOf(getString("events.equip.mode"));
+ }
+
+ public boolean isStackOnSpawn() {
+ return getBoolean("stack.on-spawn");
+ }
+
+ private boolean isEntityTypeInList(String path) {
+ ConfigList list = getList(path);
+ if (list.isInverted() && list.rawContains(type.toString())) {
+ return false;
+ }
+ for (EntityGrouping entityGrouping : EntityGrouping.values()) {
+ if (!list.rawContains(entityGrouping.toString())) {
+ continue;
+ }
+ if (entityGrouping.isEntityMemberOf(type.getEntityClass())) {
+ return !list.isInverted();
+ }
+ }
+ return list.contains(type.toString());
+ }
+
+ public NameTagInteractMode getNameTagInteractMode() {
+ return NameTagInteractMode.valueOf(getString("events.nametag.mode"));
+ }
+
+ public NameTagStackMode getNameTagStackMode() {
+ NameTagStackMode nameTagStackMode = NameTagStackMode.valueOf(getString("stack.nametag-mode"));
+ if (nameTagStackMode == NameTagStackMode.JOIN && !(getTagMode() == StackEntity.TagMode.NEARBY && isUseArmorStand())) {
+ return NameTagStackMode.IGNORE;
+ }
+ return nameTagStackMode;
+ }
+
+ public boolean isCheckCanSee() {
+ return getBoolean("stack.line-of-sight");
+ }
+
+ public EntityType getType() {
+ return type;
+ }
+
+ enum EntityGrouping {
+ HOSTILE(Monster.class, Ghast.class, Phantom.class),
+ ANIMALS(Animals.class),
+ WATER(WaterMob.class),
+ RAIDER(Raider.class),
+ BOSS(Boss.class);
+
+ final Class extends Entity>[] classes;
+ @SafeVarargs
+ EntityGrouping(Class extends Entity>... classes) {
+ this.classes = classes;
+ }
+
+ public boolean isEntityMemberOf(Class extends Entity> entity) {
+ if (entity == null) {
+ return false;
+ }
+ for (Class extends Entity> entityClass : classes) {
+ if (entityClass.isAssignableFrom(entity)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ public enum EventType {
+ BREED("breed"),
+ DYE("dye"),
+ EXPLOSION("explosion"),
+ SHEAR("shear");
+
+ final String configKey;
+ EventType(String configKey) {
+ this.configKey = configKey;
+ }
+
+ public String getConfigKey() {
+ return configKey;
+ }
+ }
+
+ public enum ListenerMode {
+ MULTIPLY,
+ SPLIT
+ }
+
+ public enum NameTagInteractMode {
+ SLICE,
+ PREVENT
+ }
+
+ public enum NameTagStackMode {
+ IGNORE,
+ DROP,
+ JOIN
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/config/EntityTranslation.java b/src/main/java/uk/antiperson/stackmob/config/EntityTranslation.java
index 21143100..4636f9f9 100644
--- a/src/main/java/uk/antiperson/stackmob/config/EntityTranslation.java
+++ b/src/main/java/uk/antiperson/stackmob/config/EntityTranslation.java
@@ -29,4 +29,9 @@ public void createFile() throws IOException {
}
save();
}
+
+ @Override
+ public void reloadConfig() throws IOException {
+ load();
+ }
}
diff --git a/src/main/java/uk/antiperson/stackmob/config/MainConfig.java b/src/main/java/uk/antiperson/stackmob/config/MainConfig.java
index 50000f06..38742db6 100644
--- a/src/main/java/uk/antiperson/stackmob/config/MainConfig.java
+++ b/src/main/java/uk/antiperson/stackmob/config/MainConfig.java
@@ -1,213 +1,115 @@
package uk.antiperson.stackmob.config;
-import org.bukkit.World;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
-import org.bukkit.entity.LivingEntity;
-import org.bukkit.event.entity.CreatureSpawnEvent;
+import org.bukkit.entity.Mob;
import uk.antiperson.stackmob.StackMob;
-import uk.antiperson.stackmob.entity.death.DeathType;
-import uk.antiperson.stackmob.entity.TagMode;
-import uk.antiperson.stackmob.listeners.ListenerMode;
+import uk.antiperson.stackmob.entity.StackEntity;
import java.io.IOException;
-import java.util.*;
+import java.util.EnumMap;
+import java.util.Map;
-public class MainConfig extends SpecialConfigFile {
+public class MainConfig {
+ private final Map map;
+ private final MainConfigFile configFile;
private final StackMob sm;
+
public MainConfig(StackMob sm) {
- super(sm, "config.yml");
this.sm = sm;
- }
-
- public int getMaxStack(EntityType type) {
- return getInt(type, "stack.max-size");
- }
-
- public boolean getStackThresholdEnabled(EntityType type) {
- return getBoolean(type, "stack.threshold.enabled");
- }
-
- public int getStackThreshold(EntityType type) {
- return getInt(type, "stack.threshold.amount");
- }
-
- public Integer[] getStackRadius(EntityType type) {
- return getList(type, "stack.merge-range").asIntList().toArray(new Integer[2]);
- }
-
- public int getStackInterval() {
- return getInt("stack.interval");
- }
-
- public String getTagFormat(EntityType type) {
- return getString(type, "display-name.format");
- }
-
- public int getTagThreshold(EntityType type) {
- return getInt(type, "display-name.threshold");
- }
-
- public TagMode getTagMode(EntityType type) {
- return TagMode.valueOf(getString(type, "display-name.visibility"));
- }
-
- public Integer[] getTagNeabyRadius() {
- return getList("display-name.nearby.range").asIntList().toArray(new Integer[2]);
- }
-
- public int getTagNearbyInterval() {
- return getInt("display-name.nearby.interval");
- }
-
- public boolean isTraitEnabled(String traitKey) {
- return getBoolean("traits." + traitKey);
- }
-
- public boolean isHookEnabled(String traitKey) {
- return getBoolean("hooks." + traitKey);
- }
-
- public boolean isDropMultiEnabled(EntityType type) {
- return getBoolean(type, "drops.enabled");
- }
-
- public boolean isDropLootTables(EntityType type) {
- return getBoolean(type, "drops.use-loot-tables");
- }
-
- public boolean isSlimeMultiEnabled(EntityType type) {
- return getBoolean(type, "multiply.slime-split");
- }
-
- public ConfigList getDropTypeBlacklist(EntityType type) {
- return getList(type, "drops.type-blacklist");
- }
-
- public ConfigList getDropReasonBlacklist(EntityType type) {
- return getList(type, "drops.reason-blacklist");
- }
-
- public ConfigList getDropItemBlacklist(EntityType type) {
- return getList(type, "drops.item-blacklist");
- }
-
- public ConfigList getDropItemOnePer(EntityType type) {
- return getList(type, "drops.one-per-stack");
- }
-
- public boolean isExpMultiEnabled(EntityType type) {
- return getBoolean(type, "experience.enabled");
- }
-
- public ConfigList getExpTypeBlacklist(EntityType type) {
- return getList(type, "experience.type-blacklist");
- }
-
- public double getExpMinBound(EntityType type) {
- return getDouble(type, "experience.multiplier-min");
- }
-
- public double getExpMaxBound(EntityType type) {
- return getDouble(type, "experience.multiplier-max");
- }
-
- public boolean isPlayerStatMulti(EntityType type) {
- return getBoolean(type, "player-stats");
- }
-
- public boolean isWaitingEnabled(EntityType type) {
- return getBoolean(type, "wait-to-stack.enabled");
- }
-
- public ConfigList getWaitingTypes(EntityType type) {
- return getList(type, "wait-to-stack.types-whitelist");
- }
-
- public ConfigList getWaitingReasons(EntityType type) {
- return getList(type, "wait-to-stack.reasons-whitelist");
- }
-
- public int getWaitingTime(EntityType type) {
- return getInt(type, "wait-to-stack.wait-time");
- }
-
- public int getMaxDeathStep(EntityType type) {
- return getInt(type, "death.STEP.max-step");
- }
-
- public int getMinDeathStep(EntityType type) {
- return getInt(type, "death.STEP.min-step");
- }
-
- public boolean isListenerEnabled(String eventKey) {
- return getBoolean("events." + eventKey);
- }
-
- public boolean removeStackDataOnDivide(String reasonKey) { return getBoolean("events.remove-stack-data." + reasonKey); }
-
- public boolean isTargetingDisabled(EntityType type) {
- return getBoolean(type, "disable-targeting.enabled");
- }
-
- public ConfigList getTargetingDisabledTypes(EntityType type) {
- return getList(type, "disable-targeting.type-blacklist");
- }
-
- public ConfigList getTargetingDisabledReasons(EntityType type) {
- return getList(type, "disable-targeting.reason-blacklist");
- }
-
- public ListenerMode getListenerMode(EntityType type, String eventKey) {
- return ListenerMode.valueOf(getString(type, "events." + eventKey + ".mode"));
- }
-
- public boolean isWorldBlacklisted(EntityType type, World world) {
- return getList(type, "worlds-blacklist").contains(world.getName());
- }
-
- public boolean isEntityBlacklisted(LivingEntity entity, CreatureSpawnEvent.SpawnReason reason) {
- if (getList(entity.getType(), "types-blacklist").contains(entity.getType().toString())) {
- return true;
+ this.map = new EnumMap<>(EntityType.class);
+ this.configFile = new MainConfigFile(sm);
+ }
+
+ public void init() throws IOException {
+ configFile.load();
+ // iterate every entity type
+ for (EntityType entityType : EntityType.values()) {
+ if (entityType.getEntityClass() == null || !Mob.class.isAssignableFrom(entityType.getEntityClass())) {
+ if (entityType != EntityType.UNKNOWN) {
+ continue;
+ }
+ }
+ // create new EntityConfig for this entity type
+ EntityConfig entityConfig = new EntityConfig(sm, entityType);
+ // populate with config contents
+ for (String key : configFile.getKeys(true)) {
+ if (key.startsWith("custom")) {
+ continue;
+ }
+ entityConfig.put(key, new ConfigValue(configFile, key, configFile.get(key)));
+ }
+ // store
+ map.put(entityType, entityConfig);
}
- if (getList(entity.getType(), "reason-blacklist").contains(reason.toString())) {
- return true;
+ // now find which config items have been overridden
+ ConfigurationSection custom = configFile.getConfigurationSection("custom");
+ if (custom == null) {
+ return;
}
- return isWorldBlacklisted(entity.getType(), entity.getWorld());
- }
-
- public DeathType getDeathType(LivingEntity dead) {
- for (String key : getDeathSection(dead)) {
- ConfigList reasons = getList(dead.getType(), "death." + key + ".reason-blacklist");
- if (reasons.contains(dead.getLastDamageCause().getCause().toString())) {
- continue;
+ // get all the top level keys in the custom section
+ for (String key : custom.getKeys(false)) {
+ EntityType toRead = EntityType.valueOf(key);
+ // determine if we are cloning another section
+ EntityType cloneType = null;
+ String clone = custom.getString(toRead + ".clone", null);
+ if (clone != null && clone.length() > 0) {
+ cloneType = EntityType.valueOf(clone);
}
- ConfigList types = getList(dead.getType(), "death." + key + ".type-blacklist");
- if (types.contains(dead.getType().toString())) {
- continue;
+ // get the entity config for this section
+ EntityConfig entityConfig = map.get(toRead);
+ // load all custom values (from the cloned entity type and this entity type) into the entity config
+ EntityType[] array = new EntityType[]{cloneType, toRead}; // order is important - cloned values may be overridden
+ for (EntityType type : array) {
+ if (type == null) {
+ continue;
+ }
+ ConfigurationSection section = configFile.getConfigurationSection("custom." + type);
+ for (String customKey : section.getKeys(true)) {
+ if (customKey.equals("clone")) {
+ continue;
+ }
+ String actualKey = "custom." + type + "." + customKey;
+ entityConfig.put(customKey, new ConfigValue(configFile, actualKey, section.get(customKey)));
+ }
}
- return DeathType.valueOf(key);
}
- throw new UnsupportedOperationException("Configuration error - unable to determine death type!");
}
- private Collection getDeathSection(LivingEntity dead) {
- TreeMap array = new TreeMap<>();
- for (String key : getConfigurationSection("death").getKeys(false)) {
- array.put(getInt(dead.getType(), "death." + key + ".priority"), key);
- }
- return array.values();
+ public void reload() throws IOException {
+ init();
+ sm.getEntityManager().getStackEntities().forEach(StackEntity::refreshConfig);
}
- @Override
- public void updateFile() throws IOException {
- if (isSet("check-area.x")) {
- sm.getLogger().info("Old config detected. Renaming to config.old and making a new one.");
- makeOld();
- sm.downloadBridge();
- return;
- }
- super.updateFile();
+ /**
+ * Returns the EntityConfig for the specified entity type
+ * @param type the type to check
+ * @return the EntityConfig for the specified entity type
+ */
+ public EntityConfig getConfig(EntityType type) {
+ return map.get(type);
}
+ /**
+ * Returns the EntityConfig for the specified entity
+ * @param entity the entity to check
+ * @return the EntityConfig for the specified entity
+ */
+ public EntityConfig getConfig(Entity entity) {
+ return getConfig(entity.getType());
+ }
+
+ /**
+ * Returns the EntityConfig whenever there is no entity present
+ * @return the EntityConfig whenever there is no entity present
+ */
+ public EntityConfig getConfig() {
+ return map.get(EntityType.UNKNOWN);
+ }
+
+ public MainConfigFile getConfigFile() {
+ return configFile;
+ }
}
diff --git a/src/main/java/uk/antiperson/stackmob/config/MainConfigFile.java b/src/main/java/uk/antiperson/stackmob/config/MainConfigFile.java
new file mode 100644
index 00000000..46fbe29f
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/config/MainConfigFile.java
@@ -0,0 +1,32 @@
+package uk.antiperson.stackmob.config;
+
+import uk.antiperson.stackmob.StackMob;
+
+import java.io.IOException;
+
+public class MainConfigFile extends ConfigFile {
+
+ private StackMob sm;
+
+ public MainConfigFile(StackMob sm) {
+ super(sm, "config.yml");
+ this.sm = sm;
+ }
+
+ @Override
+ public void updateFile() throws IOException {
+ if (isSet("check-area.x")) {
+ sm.getLogger().info("Old config detected. Renaming to config.old and making a new one.");
+ sm.getLogger().warning("You are going to loose mob stack data!!!");
+ sm.getLogger().info("I would suggest either running an older version of StackMob and use the StackMobBridge plugin to covert starts. Or just replace stacks manually.");
+ makeOld();
+ return;
+ }
+ super.updateFile();
+ }
+
+ @Override
+ public void reloadConfig() throws IOException {
+ load();
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/config/SpecialConfigFile.java b/src/main/java/uk/antiperson/stackmob/config/SpecialConfigFile.java
deleted file mode 100644
index b043fc9f..00000000
--- a/src/main/java/uk/antiperson/stackmob/config/SpecialConfigFile.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package uk.antiperson.stackmob.config;
-
-import org.apache.commons.lang.math.NumberUtils;
-import org.bukkit.entity.EntityType;
-import uk.antiperson.stackmob.StackMob;
-
-import java.util.List;
-
-public abstract class SpecialConfigFile extends ConfigFile {
-
- public SpecialConfigFile(StackMob sm, String filePath) {
- super(sm, filePath);
- }
-
- public ConfigList getList(EntityType type, String path) {
- ConfigValue configValue = get(type, path);
- return configValue.getValue() instanceof List> ? ConfigList.getConfigList(this, new ConfigValue(configValue.getPath(), configValue.getValue())) : null;
- }
-
- public boolean getBoolean(EntityType type, String path) {
- Object value = getValue(type, path);
- return value instanceof Boolean ? (Boolean) value : false;
- }
-
- public double getDouble(EntityType type, String path) {
- Object value = getValue(type, path);
- return value instanceof Double ? NumberUtils.toDouble(value.toString()) : 0;
- }
-
- public int getInt(EntityType type, String path) {
- Object value = getValue(type, path);
- return value instanceof Number ? NumberUtils.toInt(value.toString()) : 0;
- }
-
- public String getString(EntityType type, String path) {
- Object value = getValue(type, path);
- return value == null ? null : value.toString();
- }
-
- private Object getValue(EntityType type, String path) {
- return get(type, path).getValue();
- }
-
- private ConfigValue get(EntityType type, String path) {
- if (!isFileLoaded()) {
- throw new UnsupportedOperationException("Configuration file has not been loaded!");
- }
- // Check if this entity clones another entity.
- String typeName = getString("custom." + type + ".clone", type.toString()).toUpperCase();
- // Check if the specified general config path is overridden by an entity specific equivalent.
- String customPath = "custom." + typeName + "." + path;
- Object customValue = get(customPath);
- if (customValue != null) {
- return new ConfigValue(customPath, customValue);
- }
- return new ConfigValue(path, get(path));
- }
-
- /**
- * Gets the custom config path for this entity if it has been specified. If not this will just be the normal value.
- * @param type the entity type
- * @param path the config path.
- * @return the path for this config path and entity.
- */
- private String getPath(EntityType type, String path) {
- if (!isFileLoaded()) {
- throw new UnsupportedOperationException("Configuration file has not been loaded!");
- }
- // Check if the specified general config path is overridden by an entity specific equivalent.
- String customPath = "custom." + type + "." + path;
- if (isSet(customPath)) {
- return customPath;
- }
- // Check if this entity specific path is specified to clone another path.
- String clone = "custom." + type + ".clone";
- String clonePath = "custom." + getString(clone) + "." + path;
- if (isString(clonePath)) {
- return clonePath;
- }
- return path;
- }
-}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/Drops.java b/src/main/java/uk/antiperson/stackmob/entity/Drops.java
index 3b9b4528..a1f2b24b 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/Drops.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/Drops.java
@@ -6,11 +6,17 @@
import org.bukkit.entity.ExperienceOrb;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Mob;
+import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.loot.LootContext;
import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.config.ConfigList;
+import uk.antiperson.stackmob.utils.Utilities;
-import java.util.*;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
public class Drops {
@@ -24,17 +30,26 @@ public Drops(StackMob sm, StackEntity entity) {
public Map calculateDrops(int deathAmount, List originalDrops) {
Map items = new HashMap<>();
- if (!sm.getMainConfig().isDropMultiEnabled(dead.getType())) {
+ if (!sm.getMainConfig().getConfig(dead).isDropMultiEnabled()) {
return items;
}
- if (sm.getMainConfig().getDropTypeBlacklist(dead.getType()).contains(dead.getType().toString())) {
+ if (sm.getMainConfig().getConfig(dead).isDropTypeBlacklist()) {
return items;
}
- if (sm.getMainConfig().getDropReasonBlacklist(dead.getType()).contains(dead.getLastDamageCause().getCause().toString())) {
+ EntityDamageEvent lastDamageCause = dead.getLastDamageCause();
+ if (lastDamageCause == null || sm.getMainConfig().getConfig(dead).isDropReasonBlacklist(lastDamageCause.getCause())) {
return items;
}
+ boolean useLootTables = sm.getMainConfig().getConfig(dead).isDropLootTables();
+ ConfigList itemBlacklist = sm.getMainConfig().getConfig(dead).getDropItemBlacklist();
+ ConfigList dropOneItemPer = sm.getMainConfig().getConfig(dead).getDropItemOnePer();
+ LootContext lc = new LootContext.Builder(dead.getLocation()).lootedEntity(dead).killer(dead.getKiller()).build();
+ Collection genItems = originalDrops;
for (int i = 0; i < deathAmount; i++) {
- for (ItemStack is : calculateLoot(originalDrops)) {
+ if (useLootTables) {
+ genItems = ((Mob) dead).getLootTable().populateLoot(ThreadLocalRandom.current(), lc);
+ }
+ for (ItemStack is : genItems) {
if (is == null || is.getType() == Material.AIR) {
continue;
}
@@ -44,49 +59,30 @@ public Map calculateDrops(int deathAmount, List o
if (is.getType() == Material.LEAD && dead.isLeashed()) {
continue;
}
- if (sm.getMainConfig().getDropItemBlacklist(dead.getType())
- .contains(is.getType().toString())) {
+ if (itemBlacklist.contains(is.getType().toString())) {
continue;
}
- int dropAmount = is.getAmount();
- if (sm.getMainConfig().getDropItemOnePer(dead.getType())
- .contains(is.getType().toString())) {
- dropAmount = 1;
- }
+ int dropAmount = dropOneItemPer.contains(is.getType().toString()) ? 1 : is.getAmount();
is.setAmount(1);
- if (items.containsKey(is)) {
- items.put(is, items.get(is) + dropAmount);
- continue;
- }
- items.put(is, dropAmount);
+ items.compute(is, (key, amount) -> amount == null ? dropAmount : amount + dropAmount);
}
}
return items;
}
- private Collection calculateLoot(List originalDrops) {
- if (!sm.getMainConfig().isDropLootTables(dead.getType())) {
- return originalDrops;
- }
- LootContext lc = new LootContext.Builder(dead.getLocation()).lootedEntity(dead).killer(dead.getKiller()).build();
- return ((Mob) dead).getLootTable().populateLoot(ThreadLocalRandom.current(), lc);
- }
-
public static void dropItems(Location location, Map items) {
items.forEach((stack, amount) -> dropItem(location, stack, amount));
}
public static void dropItem(Location location, ItemStack stack, int amount) {
- double inStacks = (double) amount / (double) stack.getMaxStackSize();
- double floor = Math.floor(inStacks);
- double leftOver = inStacks - floor;
- for (int i = 0; i < floor; i++) {
- stack.setAmount(stack.getMaxStackSize());
- location.getWorld().dropItemNaturally(location, stack);
- }
- if (leftOver > 0) {
- stack.setAmount((int) Math.round(leftOver * stack.getMaxStackSize()));
- location.getWorld().dropItemNaturally(location, stack);
+ dropItem(location, stack, amount, false);
+ }
+
+ public static void dropItem(Location location, ItemStack stack, int amount, boolean above) {
+ Location dropLocation = above ? location.add(0,1,0) : location;
+ for (int itemAmount : Utilities.split(amount, stack.getMaxStackSize())) {
+ stack.setAmount(itemAmount);
+ location.getWorld().dropItem(dropLocation, stack);
}
}
@@ -103,14 +99,14 @@ private boolean dropIsArmor(ItemStack stack) {
}
public int calculateDeathExperience(int deadCount, int exp) {
- if (!sm.getMainConfig().isExpMultiEnabled(dead.getType())) {
+ if (!sm.getMainConfig().getConfig(dead).isExpMultiEnabled()) {
return exp;
}
- if (sm.getMainConfig().getExpTypeBlacklist(dead.getType()).contains(dead.getType())) {
+ if (sm.getMainConfig().getConfig(dead).isExpTypeBlacklist()) {
return exp;
}
- double minMulti = sm.getMainConfig().getExpMinBound(dead.getType()) * exp;
- double maxMulti = sm.getMainConfig().getExpMaxBound(dead.getType()) * exp;
+ double minMulti = sm.getMainConfig().getConfig(dead).getExpMinBound() * exp;
+ double maxMulti = sm.getMainConfig().getConfig(dead).getExpMaxBound() * exp;
return exp + calculateExperience(minMulti, maxMulti, deadCount);
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/EntityFood.java b/src/main/java/uk/antiperson/stackmob/entity/EntityFood.java
new file mode 100644
index 00000000..ac1e38d8
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/entity/EntityFood.java
@@ -0,0 +1,89 @@
+package uk.antiperson.stackmob.entity;
+
+import org.bukkit.Material;
+import org.bukkit.Tag;
+import org.bukkit.entity.Animals;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Tameable;
+import uk.antiperson.stackmob.utils.Utilities;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.function.Predicate;
+
+public enum EntityFood {
+ COW(Material.WHEAT),
+ SHEEP(Material.WHEAT),
+ MUSHROOM_COW(Material.WHEAT),
+ PIG(Material.CARROT, Material.BEETROOT, Material.POTATO),
+ CHICKEN(Material.WHEAT_SEEDS, Material.BEETROOT_SEEDS, Material.MELON_SEEDS, Material.PUMPKIN_SEEDS),
+ HORSE(Tameable::isTamed, Material.GOLDEN_CARROT, Material.GOLDEN_APPLE),
+ WOLF(Tameable::isTamed, Material.BEEF,
+ Material.CHICKEN,
+ Material.COD,
+ Material.MUTTON,
+ Material.PORKCHOP,
+ Material.RABBIT,
+ Material.SALMON,
+ Material.COOKED_BEEF,
+ Material.COOKED_CHICKEN,
+ Material.COOKED_COD,
+ Material.COOKED_MUTTON,
+ Material.COOKED_PORKCHOP,
+ Material.COOKED_RABBIT,
+ Material.COOKED_SALMON),
+ OCELOT(Material.SALMON, Material.COD, Material.PUFFERFISH, Material.TROPICAL_FISH),
+ RABBIT(Material.CARROT, Material.GOLDEN_CARROT, Material.DANDELION),
+ LLAMA(Material.HAY_BLOCK),
+ TURTLE(Material.SEAGRASS),
+ PANDA(Material.BAMBOO),
+ FOX(Material.SWEET_BERRIES),
+ CAT(Tameable::isTamed, Material.SALMON, Material.COD),
+ BEE(Tag.FLOWERS.getValues().toArray(new Material[0])),
+ HOGLIN(Material.CRIMSON_FUNGUS),
+ INVALID;
+
+ private final Predicate predicate;
+ private final Material[] foods;
+ EntityFood(Predicate predicate, Material... foods) {
+ this.predicate = predicate;
+ this.foods = foods;
+ }
+
+ EntityFood(Material... foods) {
+ this.predicate = null;
+ this.foods = foods;
+ }
+
+ public Material[] getFoods() {
+ return foods;
+ }
+
+ public Predicate getPredicate() {
+ return predicate;
+ }
+
+ public static EntityFood matchFood(EntityType type) {
+ try {
+ return EntityFood.valueOf(type.toString());
+ } catch (IllegalArgumentException e) {
+ return EntityFood.INVALID;
+ }
+ }
+
+ public static boolean isCorrectFood(Entity entity, Material type) {
+ if (Utilities.isVersionAtLeast(Utilities.MinecraftVersion.V1_17)) {
+ return ((Animals) entity).isBreedItem(type);
+ }
+ EntityFood food = matchFood(entity.getType());
+ if (food.getPredicate() != null && !food.getPredicate().getClass().isAssignableFrom(entity.getClass())) {
+ return false;
+ }
+ for (Material material : food.getFoods()) {
+ if (material == type) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/EntityManager.java b/src/main/java/uk/antiperson/stackmob/entity/EntityManager.java
index 08d78456..c59c6a13 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/EntityManager.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/EntityManager.java
@@ -1,25 +1,106 @@
package uk.antiperson.stackmob.entity;
+import org.bukkit.Bukkit;
+import org.bukkit.Chunk;
+import org.bukkit.World;
+import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
+import org.bukkit.entity.Mob;
import org.bukkit.persistence.PersistentDataType;
import uk.antiperson.stackmob.StackMob;
+import java.util.Collection;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
public class EntityManager {
private final StackMob sm;
+ private final Map stackEntities;
+
public EntityManager(StackMob sm) {
this.sm = sm;
+ stackEntities = new ConcurrentHashMap<>();
}
public boolean isStackedEntity(LivingEntity entity) {
- return entity.getPersistentDataContainer().has(sm.getStackKey(), PersistentDataType.INTEGER);
+ return stackEntities.containsKey(entity.getUniqueId());
+ }
+
+ public Collection getStackEntities() {
+ return stackEntities.values();
}
public StackEntity getStackEntity(LivingEntity entity) {
- return new StackEntity(sm, entity);
+ return stackEntities.get(entity.getUniqueId());
+ }
+
+ public void registerAllEntities() {
+ for (World world : Bukkit.getWorlds()) {
+ for (Chunk chunk : world.getLoadedChunks()) {
+ registerStackedEntities(chunk);
+ }
+ }
+ }
+
+ public void unregisterAllEntities() {
+ for (World world : Bukkit.getWorlds()) {
+ for (Chunk chunk : world.getLoadedChunks()) {
+ unregisterStackedEntities(chunk);
+ }
+ }
+ }
+
+ public boolean hasStackData(Entity entity) {
+ return entity.getPersistentDataContainer().has(sm.getStackKey(), PersistentDataType.INTEGER);
}
- public boolean isWaiting(LivingEntity entity) {
- return entity.getPersistentDataContainer().has(sm.getWaitKey(), PersistentDataType.INTEGER);
+ public void registerStackedEntities(Chunk chunk) {
+ for (Entity entity : chunk.getEntities()) {
+ if (!(entity instanceof Mob)) {
+ continue;
+ }
+ if (!hasStackData(entity)) {
+ continue;
+ }
+ sm.getEntityManager().registerStackedEntity((LivingEntity) entity);
+ }
}
+
+ public void unregisterStackedEntities(Chunk chunk) {
+ for (Entity entity : chunk.getEntities()) {
+ if (!(entity instanceof Mob)) {
+ continue;
+ }
+ StackEntity stackEntity = sm.getEntityManager().getStackEntity((LivingEntity) entity);
+ if (stackEntity == null) {
+ continue;
+ }
+ sm.getEntityManager().unregisterStackedEntity(stackEntity);
+ }
+ }
+
+ public StackEntity registerStackedEntity(LivingEntity entity) {
+ StackEntity stackEntity = new StackEntity(sm, entity);
+ stackEntities.put(entity.getUniqueId(), stackEntity);
+ return stackEntity;
+ }
+
+ public void registerStackedEntity(StackEntity entity) {
+ stackEntities.put(entity.getEntity().getUniqueId(), entity);
+ }
+
+ public void unregisterStackedEntity(LivingEntity entity) {
+ StackEntity stackEntity = getStackEntity(entity);
+ if (stackEntity == null) {
+ throw new UnsupportedOperationException("Attempted to unregister entity that isn't stacked!");
+ }
+ unregisterStackedEntity(stackEntity);
+ }
+
+ public void unregisterStackedEntity(StackEntity stackEntity) {
+ stackEntities.remove(stackEntity.getEntity().getUniqueId());
+ }
+
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/StackEntity.java b/src/main/java/uk/antiperson/stackmob/entity/StackEntity.java
index 9b9952aa..24c86505 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/StackEntity.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/StackEntity.java
@@ -1,20 +1,55 @@
package uk.antiperson.stackmob.entity;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import org.bukkit.FluidCollisionMode;
+import org.bukkit.Location;
+import org.bukkit.Material;
import org.bukkit.World;
+import org.bukkit.entity.Animals;
+import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
+import org.bukkit.entity.Player;
import org.bukkit.event.entity.CreatureSpawnEvent;
+import org.bukkit.inventory.EquipmentSlot;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.util.RayTraceResult;
+import org.bukkit.util.Vector;
import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.config.EntityConfig;
+import uk.antiperson.stackmob.entity.tags.DisplayTag;
import uk.antiperson.stackmob.events.EventHelper;
+import uk.antiperson.stackmob.hook.StackableMobHook;
import uk.antiperson.stackmob.utils.Utilities;
+import java.util.HashSet;
+import java.util.Set;
+
public class StackEntity {
private final LivingEntity entity;
+ private final EntityManager entityManager;
private final StackMob sm;
+ private boolean waiting;
+ private boolean forgetOnSpawn;
+ private boolean removed;
+ private Location lastLocation;
+ private int waitCount;
+ private int stackSize;
+ private int lastLocationTimeout;
+ private Set equiptItems;
+ private Tag tag;
+ private DisplayTag displayTag;
+ private EntityConfig entityConfig;
+
public StackEntity(StackMob sm, LivingEntity entity) {
this.sm = sm;
this.entity = entity;
+ this.entityManager = sm.getEntityManager();
+ this.displayTag = new DisplayTag(sm, this);
}
/**
@@ -39,6 +74,7 @@ public void setSize(int newSize, boolean update) {
newSize = getMaxSize();
}
entity.getPersistentDataContainer().set(sm.getStackKey(), PersistentDataType.INTEGER, newSize);
+ stackSize = newSize;
if (update) {
getTag().update();
}
@@ -46,36 +82,117 @@ public void setSize(int newSize, boolean update) {
public void removeStackData() {
entity.getPersistentDataContainer().remove(sm.getStackKey());
- getTag().update();
entity.setCustomNameVisible(false);
+ entityManager.unregisterStackedEntity(this);
+ if (!isSingle()) {
+ getTag().update();
+ if (getDisplayTag().exists()) {
+ getDisplayTag().remove();
+ }
+ }
}
- public boolean shouldWait(CreatureSpawnEvent.SpawnReason spawnReason) {
- if (!sm.getMainConfig().isWaitingEnabled(getEntity().getType())) {
- return false;
+ /**
+ * Returns the location of the entity where it was last checked for stacking.
+ * @return the location of the entity where it was last checked for stacking, null if 'stack.check-location' is disabled.
+ */
+ public Location getLastLocation() {
+ if (lastLocation == null && getEntityConfig().isCheckHasMoved()) {
+ lastLocation = entity.getLocation();
}
- if (!sm.getMainConfig().getWaitingTypes(getEntity().getType()).contains(getEntity().getType().toString())) {
+ return lastLocation;
+ }
+
+ /**
+ * Sets the location of the entity where it was last checked for stacking.
+ * @param lastLocation the location of the entity where it was last checked for stacking.
+ */
+ public void setLastLocation(Location lastLocation) {
+ this.lastLocation = lastLocation;
+ }
+
+ public boolean skipLastLocation() {
+ if (lastLocationTimeout == 0) {
+ lastLocationTimeout = getEntityConfig().getCheckHasMovedTimeout();
+ return true;
+ }
+ lastLocationTimeout--;
+ return false;
+ }
+
+ /**
+ * Returns whether the entity will have its stack data removed on spawn.
+ * See {@link #setForgetOnSpawn(boolean)} for further details.
+ * @return whether the entity will have its stack data removed on spawn.
+ */
+ public boolean isForgetOnSpawn() {
+ return forgetOnSpawn;
+ }
+
+ /**
+ * Make it so that the entity will have its stack data removed in the spawn event.
+ * This will only work if the entity already has stack data.
+ * If you are a plugin developer, use {@link uk.antiperson.stackmob.events.StackSpawnEvent} instead.
+ * @param forgetOnSpawn whether the entity should have its stack data removed in the spawn event.
+ */
+ public void setForgetOnSpawn(boolean forgetOnSpawn) {
+ this.forgetOnSpawn = forgetOnSpawn;
+ }
+
+ /**
+ * In order to not break mob grinders, stacked entities can have a waiting status.
+ * This waiting status means that this stacked entity will be ignored on all stacking attempts until the count reaches 0
+ * @return whether this entity is currently waiting
+ */
+ public boolean isWaiting() {
+ return waiting;
+ }
+
+ /**
+ * Gets the wait count for this entity.
+ * @return the wait count for this entity
+ */
+ public int getWaitCount() {
+ return waitCount;
+ }
+
+ /**
+ * Whether this entity should wait.
+ * @param spawnReason the spawn reason of the entity.
+ * @return whether this entity should wait.
+ */
+ public boolean shouldWait(CreatureSpawnEvent.SpawnReason spawnReason) {
+ if (!getEntityConfig().isWaitingEnabled()) {
return false;
}
- if (!sm.getMainConfig().getWaitingReasons(getEntity().getType()).contains(spawnReason.toString())) {
+ if (!getEntityConfig().isWaitingTypes()) {
return false;
}
- return true;
+ return getEntityConfig().isWaitingReasons(spawnReason);
}
+ /**
+ * In order to not break mob grinders, stacked entities can have a waiting status.
+ * This waiting status means that this stacked entity will be ignored on all stacking attempts until the count reaches 0.
+ */
public void makeWait() {
- int time = sm.getMainConfig().getWaitingTime(getEntity().getType());
- getEntity().getPersistentDataContainer().set(sm.getWaitKey(), PersistentDataType.INTEGER, time);
+ if (isWaiting()) {
+ throw new UnsupportedOperationException("Stack is already waiting!");
+ }
+ waitCount = getEntityConfig().getWaitingTime();
+ waiting = true;
}
+ /**
+ * Increment the waiting count.
+ */
public void incrementWait() {
- int currentWaiting = getEntity().getPersistentDataContainer().getOrDefault(sm.getWaitKey(), PersistentDataType.INTEGER, 0);
- if (currentWaiting < 1) {
- getEntity().getPersistentDataContainer().remove(sm.getWaitKey());
+ if (waitCount < 1) {
+ waiting = false;
setSize(1);
return;
}
- getEntity().getPersistentDataContainer().set(sm.getWaitKey(), PersistentDataType.INTEGER, currentWaiting - 1);
+ waitCount -= 1;
}
/**
@@ -91,7 +208,10 @@ public void incrementSize(int increment) {
* @return the current stack size for this entity.
*/
public int getSize() {
- return entity.getPersistentDataContainer().getOrDefault(sm.getStackKey(), PersistentDataType.INTEGER, 1);
+ if (stackSize == 0) {
+ stackSize = getEntity().getPersistentDataContainer().getOrDefault(sm.getStackKey(), PersistentDataType.INTEGER, 1);
+ }
+ return stackSize;
}
/**
@@ -99,14 +219,30 @@ public int getSize() {
* @return the maximum stack size
*/
public int getMaxSize() {
- return sm.getMainConfig().getMaxStack(getEntity().getType());
+ return getEntityConfig().getMaxStack();
}
/**
* Removes this entity.
*/
public void remove() {
+ remove(true);
+ }
+
+ public void remove(boolean unregister) {
+ setRemoved();
entity.remove();
+ if (unregister) {
+ entityManager.unregisterStackedEntity(this);
+ }
+ if (getEntity().isLeashed()) {
+ ItemStack leash = new ItemStack(Material.LEAD, 1);
+ getWorld().dropItemNaturally(entity.getLocation(), leash);
+ }
+ dropEquipItems();
+ if (getDisplayTag().exists()) {
+ getDisplayTag().remove();
+ }
}
/**
@@ -130,7 +266,10 @@ public World getWorld() {
* @return a new instance of Tag for this entity.
*/
public Tag getTag() {
- return new Tag(sm, this);
+ if (tag == null) {
+ tag = new Tag();
+ }
+ return tag;
}
/**
@@ -154,54 +293,98 @@ public boolean isMaxSize() {
* @param nearby another entity
* @return if the given entity and this entity should stack.
*/
- public boolean checkNearby(StackEntity nearby) {
+ public boolean match(StackEntity nearby) {
if (getEntity().getType() != nearby.getEntity().getType()) {
return false;
}
- if (nearby.isMaxSize()) {
- return false;
- }
if (sm.getTraitManager().checkTraits(this, nearby)) {
return false;
}
if (sm.getHookManager().checkHooks(this, nearby)) {
return false;
}
- if (nearby.getEntity().isDead() || getEntity().isDead()) {
- return false;
+ return getEntityConfig().isCheckCanSee() && rayTraceStack(nearby);
+ }
+
+ public boolean canStack() {
+ if (hasEquipItem()) {
+ if (getEntityConfig().getEquipItemMode() == EquipItemMode.PREVENT_STACK) {
+ return false;
+ }
}
- return true;
+ return !getEntity().isDead() && !isMaxSize() && !isWaiting();
}
/**
* Merge this stack with another stack, providing they are similar.
* @param toMerge stack to merge with.
- * @return whether the merge was successful
+ * @param unregister whether to unregister the entity that is removed.
+ * @return the entity that was removed
*/
- public boolean merge(StackEntity toMerge) {
- StackEntity entity1 = toMerge.getSize() < getSize() ? toMerge : this;
- StackEntity entity2 = toMerge.getSize() < getSize() ? this : toMerge;
- if (EventHelper.callStackMergeEvent(entity1, entity2).isCancelled()) {
- return false;
+ public StackEntity merge(StackEntity toMerge, boolean unregister) {
+ boolean toMergeBigger = toMerge.getSize() > getSize();
+ final StackEntity smallest = toMergeBigger ? this : toMerge;
+ final StackEntity biggest = toMergeBigger ? toMerge : this;
+ if (EventHelper.callStackMergeEvent(smallest, biggest).isCancelled()) {
+ return null;
}
- int totalSize = entity1.getSize() + entity2.getSize();
- if (totalSize > getMaxSize()) {
- toMerge.setSize(totalSize - entity2.getMaxSize());
- setSize(entity2.getMaxSize());
- return true;
+ final int totalSize = smallest.getSize() + biggest.getSize();
+ final int maxSize = getMaxSize();
+ if (totalSize > maxSize) {
+ smallest.setSize(totalSize - maxSize);
+ biggest.setSize(maxSize);
+ return null;
+ }
+ mergePotionEffects(biggest, smallest);
+ dropNameTag(biggest, smallest);
+ biggest.incrementSize(smallest.getSize());
+ smallest.remove(unregister);
+ return smallest;
+ }
+
+ public void dropNameTag(StackEntity keep, StackEntity removed) {
+ if (removed.getEntity().getCustomName() == null) {
+ return;
+ }
+ switch (getEntityConfig().getNameTagStackMode()) {
+ case IGNORE:
+ break;
+ case DROP:
+ ItemStack itemStack = new ItemStack(Material.NAME_TAG);
+ ItemMeta itemMeta = itemStack.getItemMeta();
+ itemMeta.setDisplayName(removed.getEntity().getCustomName());
+ itemStack.setItemMeta(itemMeta);
+ getWorld().dropItemNaturally(removed.getEntity().getLocation(), itemStack);
+ break;
+ case JOIN:
+ String customName = keep.getEntity().getCustomName();
+ if (customName == null || customName.length() == 0) {
+ keep.getEntity().setCustomName(removed.getEntity().getCustomName());
+ break;
+ }
+ customName += " - " + removed.getEntity().getCustomName();
+ keep.getEntity().setCustomName(customName);
+ break;
+ }
+ }
+
+ public void mergePotionEffects(StackEntity toKeep, StackEntity toRemove) {
+ if (!getEntityConfig().isTraitEnabled("potion-effect")) {
+ return;
+ }
+ for (PotionEffect potionEffect : toRemove.getEntity().getActivePotionEffects()) {
+ double combinedSize = toKeep.getSize() + toRemove.getSize();
+ double percentOfTotal = toRemove.getSize() / combinedSize;
+ int newDuration = (int) Math.ceil(potionEffect.getDuration() * percentOfTotal);
+ PotionEffect newPotion = new PotionEffect(potionEffect.getType(), newDuration, potionEffect.getAmplifier());
+ toKeep.getEntity().addPotionEffect(newPotion);
}
- entity2.incrementSize(entity1.getSize());
- entity1.remove();
- return true;
}
public StackEntity splitIfNotEnough(int itemAmount) {
// If there is not enough food, then spawn a new stack with the remaining.
if (getSize() > itemAmount) {
- StackEntity notFed = duplicate();
- notFed.setSize(getSize() - itemAmount);
- setSize(itemAmount);
- return notFed;
+ return slice(itemAmount);
}
return null;
}
@@ -211,32 +394,241 @@ public StackEntity splitIfNotEnough(int itemAmount) {
* @return a clone of this entity.
*/
public StackEntity duplicate() {
+ LivingEntity clone = spawnClone();
+ StackEntity cloneStack = sm.getEntityManager().registerStackedEntity(clone);
+ cloneStack.setSize(1);
+ duplicateTraits(cloneStack);
+ if (getEntityConfig().isUseArmorStand() && getEntityConfig().getTagMode() == TagMode.NEARBY) {
+ clone.setCustomName(getEntity().getCustomName());
+ }
+ return cloneStack;
+ }
+
+ public void spawnChild(int kidAmount) {
+ // Spawn the kid
+ StackEntity kid;
+ if (Utilities.isVersionAtLeast(Utilities.MinecraftVersion.V1_19_4) && getEntity().getType() == EntityType.FROG) {
+ // tadpoles and frogs are separate entities
+ LivingEntity tadpole = spawn(EntityType.TADPOLE);
+ kid = sm.getEntityManager().registerStackedEntity(tadpole);
+ kid.setSize(1);
+ duplicateTraits(kid);
+ } else {
+ kid = duplicate();
+ ((Animals) kid.getEntity()).setBaby();
+ }
+ kid.setSize(kidAmount);
+ }
+
+ private void duplicateTraits(StackEntity cloneStack) {
+ LivingEntity clone = cloneStack.getEntity();
+ sm.getTraitManager().applyTraits(cloneStack, this);
+ sm.getHookManager().onSpawn(cloneStack);
+ // Remove equipment if is a drowned
+ if (Utilities.isPaper() && clone.getEntitySpawnReason() == CreatureSpawnEvent.SpawnReason.DROWNED) {
+ for (EquipmentSlot equipmentSlot : Utilities.HAND_SLOTS) {
+ if (clone.getEquipment() == null) {
+ break;
+ }
+ for (Material material : Utilities.DROWNED_MATERIALS) {
+ if (clone.getEquipment().getItem(equipmentSlot).getType() != material) {
+ continue;
+ }
+ clone.getEquipment().setItem(equipmentSlot, null, true);
+ }
+ }
+ }
+ }
+
+ private LivingEntity spawnClone() {
LivingEntity entity = sm.getHookManager().spawnClone(getEntity().getLocation(), this);
- CreatureSpawnEvent.SpawnReason spawnReason = Utilities.isPaper() ? getEntity().getEntitySpawnReason() : CreatureSpawnEvent.SpawnReason.CUSTOM;
- entity = entity == null ? (LivingEntity) getWorld().spawnEntity(getEntity().getLocation(), getEntity().getType(), spawnReason) : entity;
- StackEntity stackEntity = sm.getEntityManager().getStackEntity(entity);
- stackEntity.setSize(1);
- sm.getTraitManager().applyTraits(stackEntity, this);
- sm.getHookManager().onSpawn(stackEntity);
- return stackEntity;
+ if (entity != null) {
+ return entity;
+ }
+ return spawn(getEntity().getType());
+ }
+
+ private LivingEntity spawn(EntityType entityType) {
+ if (Utilities.isPaper()) {
+ return (LivingEntity) getWorld().spawnEntity(getEntity().getLocation(), entityType, getEntity().getEntitySpawnReason());
+ }
+ return (LivingEntity) getWorld().spawnEntity(getEntity().getLocation(), entityType);
}
public boolean isSingle() {
- return getSize() < 2;
+ return getSize() == 1;
}
/**
- * Makes this stack singular and spawns another stack with the remaining stack size.
+ * Makes this stack smaller by 1 and spawns another stack with the remaining stack size.
* @return stack with the remaining stack size.
*/
public StackEntity slice() {
+ return slice(1);
+ }
+
+ /**
+ * Makes this stack smaller and spawns another stack with the remaining stack size.
+ * @param amount amount to
+ * @return stack with the remaining stack size.
+ */
+ public StackEntity slice(int amount) {
if (isSingle()) {
throw new UnsupportedOperationException("Stack size must be greater than 1 to slice!");
}
+ if (amount >= getSize()) {
+ throw new UnsupportedOperationException("Slice amount is bigger than the stack size!");
+ }
StackEntity duplicate = duplicate();
- duplicate.setSize(getSize() - 1);
- setSize(1);
+ duplicate.setSize(getSize() - amount);
+ setSize(amount);
+ if (getEntity().isLeashed()) {
+ duplicate.getEntity().setLeashHolder(getEntity().getLeashHolder());
+ getEntity().setLeashHolder(null);
+ }
return duplicate;
}
+ public Set getEquiptItems() {
+ return equiptItems;
+ }
+
+ public boolean hasEquipItem() {
+ return getEquiptItems() != null && !getEquiptItems().isEmpty();
+ }
+
+ public void addEquipItem(ItemStack equipt) {
+ if (!hasEquipItem()) {
+ equiptItems = new HashSet<>();
+ }
+ getEquiptItems().add(equipt);
+ }
+
+ private void dropEquipItems() {
+ if (!hasEquipItem()) {
+ return;
+ }
+ if (getEntityConfig().getEquipItemMode() != EquipItemMode.DROP_ITEMS) {
+ return;
+ }
+ for (ItemStack itemStack : getEquiptItems()) {
+ getEntity().getWorld().dropItemNaturally(getEntity().getLocation(), itemStack);
+ }
+ }
+
+ public boolean isRemoved() {
+ return removed;
+ }
+
+ private void setRemoved() {
+ this.removed = true;
+ }
+
+ public void refreshConfig() {
+ entityConfig = sm.getMainConfig().getConfig(getEntity());
+ }
+
+ public EntityConfig getEntityConfig() {
+ if (entityConfig == null) {
+ refreshConfig();
+ }
+ return entityConfig;
+ }
+
+ public boolean rayTraceStack(StackEntity stackEntity) {
+ return rayTrace(stackEntity.getEntity());
+ }
+
+ public boolean rayTracePlayer(Player player) {
+ return rayTrace(player);
+ }
+
+ /**
+ * Return whether this stack entity can be seen by the supplied entity
+ * @param livingEntity the living entity
+ * @return whether this stack entity can be seen by the supplied entity
+ */
+ private boolean rayTrace(LivingEntity livingEntity) {
+ if (getEntity().getEyeLocation().getWorld() != livingEntity.getWorld()) {
+ return false;
+ }
+ Vector resultant = getEntity().getEyeLocation().toVector().subtract(livingEntity.getEyeLocation().toVector());
+ double distance = livingEntity.getEyeLocation().distance(getEntity().getEyeLocation());
+ if (distance == 0 || resultant.lengthSquared() == 0) {
+ return true;
+ }
+ RayTraceResult result = livingEntity.getWorld().rayTraceBlocks(livingEntity.getEyeLocation(), resultant, distance, FluidCollisionMode.NEVER, true);
+ return result == null || result.getHitBlock() == null;
+ }
+
+ public DisplayTag getDisplayTag() {
+ return displayTag;
+ }
+
+ public enum EquipItemMode {
+ IGNORE,
+ DROP_ITEMS,
+ PREVENT_STACK
+ }
+
+ public enum TagMode {
+ ALWAYS,
+ HOVER,
+ NEARBY
+ }
+
+ public class Tag {
+
+ private Component displayName;
+
+ public void update() {
+ LivingEntity entity = getEntity();
+ int threshold = getEntityConfig().getTagThreshold();
+ if (getSize() <= threshold) {
+ if (getEntityConfig().isUseArmorStand() && getEntityConfig().getTagMode() == TagMode.NEARBY) {
+ if (getDisplayTag().exists()) {
+ getDisplayTag().remove();
+ }
+ return;
+ }
+ entity.customName(null);
+ entity.setCustomNameVisible(false);
+ return;
+ }
+ String format = getEntityConfig().getTagFormat();
+ format = format.replace("%type%", getEntityName());
+ format = format.replace("%size%", getSize() + "");
+ displayName = Utilities.createComponent(format);
+ if (getEntityConfig().isUseArmorStand() && getEntityConfig().getTagMode() == TagMode.NEARBY) {
+ if (!getDisplayTag().exists()) {
+ getDisplayTag().spawn();
+ }
+ getDisplayTag().updateName(displayName);
+ return;
+ }
+ entity.customName(displayName);
+ if (getEntityConfig().getTagMode() == TagMode.ALWAYS) {
+ entity.setCustomNameVisible(true);
+ }
+ }
+
+ private String getEntityName() {
+ LivingEntity entity = getEntity();
+ String typeString = sm.getEntityTranslation().getTranslatedName(entity.getType());
+ if (typeString != null && typeString.length() > 0) {
+ return typeString;
+ }
+ StackableMobHook smh = sm.getHookManager().getApplicableHook(StackEntity.this);
+ typeString = smh != null ? smh.getDisplayName(entity) : entity.getType().toString();
+ typeString = typeString == null ? entity.getType().toString() : typeString;
+ return Utilities.capitalizeString(Utilities.filter(typeString));
+ }
+
+ public Component getDisplayName() {
+ if (displayName == null) {
+ update();
+ }
+ return displayName;
+ }
+ }
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/Tag.java b/src/main/java/uk/antiperson/stackmob/entity/Tag.java
deleted file mode 100644
index 14e05961..00000000
--- a/src/main/java/uk/antiperson/stackmob/entity/Tag.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package uk.antiperson.stackmob.entity;
-
-import org.apache.commons.lang.StringUtils;
-import org.apache.commons.lang.WordUtils;
-import org.bukkit.ChatColor;
-import org.bukkit.entity.LivingEntity;
-import org.bukkit.entity.Player;
-import uk.antiperson.stackmob.StackMob;
-import uk.antiperson.stackmob.hook.StackableMobHook;
-import uk.antiperson.stackmob.hook.hooks.ProtocolLibHook;
-import uk.antiperson.stackmob.utils.NMSHelper;
-import uk.antiperson.stackmob.utils.Utilities;
-
-import java.util.logging.Level;
-
-public class Tag {
-
- private final StackEntity stackEntity;
- private final StackMob sm;
- public Tag(StackMob sm, StackEntity stackEntity) {
- this.stackEntity = stackEntity;
- this.sm = sm;
- }
-
- public void update() {
- LivingEntity entity = stackEntity.getEntity();
- int threshold = sm.getMainConfig().getTagThreshold(entity.getType());
- if (stackEntity.getSize() <= threshold) {
- entity.setCustomName(null);
- entity.setCustomNameVisible(false);
- return;
- }
- String typeString = sm.getEntityTranslation().getTranslatedName(entity.getType());
- if (typeString.length() == 0) {
- StackableMobHook smh = sm.getHookManager().getApplicableHook(stackEntity);
- typeString = smh != null ? smh.getDisplayName(entity) : entity.getType().toString();
- typeString = typeString == null ? entity.getType().toString() : typeString;
- typeString = WordUtils.capitalizeFully(typeString.replaceAll("[^A-Za-z0-9]", " "));
- }
- String displayName = sm.getMainConfig().getTagFormat(entity.getType());
- displayName = StringUtils.replace(displayName, "%type%", typeString);
- displayName = StringUtils.replace(displayName, "%size%", stackEntity.getSize() + "");
- displayName = ChatColor.translateAlternateColorCodes('&', displayName);
- entity.setCustomName(displayName);
- if (sm.getMainConfig().getTagMode(entity.getType()) == TagMode.ALWAYS) {
- entity.setCustomNameVisible(true);
- }
- }
-
- public void sendPacket(Player player, boolean tagVisible) {
- if (!Utilities.isNewBukkit()) {
- ProtocolLibHook protocolLibHook = sm.getHookManager().getProtocolLibHook();
- if (protocolLibHook == null) {
- return;
- }
- protocolLibHook.sendPacket(player, stackEntity.getEntity(), tagVisible);
- return;
- }
- try {
- NMSHelper.sendPacket(player, stackEntity.getEntity(), tagVisible);
- } catch (NoSuchFieldException | IllegalAccessException e) {
- e.printStackTrace();
- sm.getLogger().log(Level.WARNING,"An error occurred while sending packet. Is StackMob updated to support your server version?");
- }
- }
-}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/TagMode.java b/src/main/java/uk/antiperson/stackmob/entity/TagMode.java
deleted file mode 100644
index e542a2d5..00000000
--- a/src/main/java/uk/antiperson/stackmob/entity/TagMode.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package uk.antiperson.stackmob.entity;
-
-public enum TagMode {
- ALWAYS,
- HOVER,
- NEARBY
-}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/death/KillStep.java b/src/main/java/uk/antiperson/stackmob/entity/death/KillStep.java
index 47569b4e..60fb2e72 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/death/KillStep.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/death/KillStep.java
@@ -14,8 +14,8 @@ public KillStep(StackMob sm, StackEntity dead) {
@Override
public int calculateStep() {
- int maxStep = getStackMob().getMainConfig().getMaxDeathStep(getEntity().getType());
- int minStep = getStackMob().getMainConfig().getMinDeathStep(getEntity().getType());
- return ThreadLocalRandom.current().nextInt(minStep, maxStep);
+ int maxStep = getDead().getEntityConfig().getMaxDeathStep();
+ int minStep = getDead().getEntityConfig().getMinDeathStep();
+ return minStep == maxStep ? maxStep : ThreadLocalRandom.current().nextInt(minStep, maxStep);
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/death/KillStepDamage.java b/src/main/java/uk/antiperson/stackmob/entity/death/KillStepDamage.java
index d9a7e6f0..062ecdc5 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/death/KillStepDamage.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/death/KillStepDamage.java
@@ -1,25 +1,38 @@
package uk.antiperson.stackmob.entity.death;
import org.bukkit.attribute.Attribute;
+import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.LivingEntity;
+import org.bukkit.potion.PotionEffectType;
import uk.antiperson.stackmob.StackMob;
import uk.antiperson.stackmob.entity.StackEntity;
+import uk.antiperson.stackmob.entity.traits.TraitMetadata;
+import uk.antiperson.stackmob.entity.traits.trait.Potion;
import uk.antiperson.stackmob.hook.StackableMobHook;
-import uk.antiperson.stackmob.hook.hooks.MythicMobsHook;
+import uk.antiperson.stackmob.hook.hooks.MythicMobsStackHook;
public class KillStepDamage extends DeathMethod {
private double leftOverDamage;
+
public KillStepDamage(StackMob sm, StackEntity dead) {
super(sm, dead);
}
@Override
public int calculateStep() {
+ if (getDead().getEntity().getLastDamageCause() == null) {
+ return 1;
+ }
double healthBefore = ((LivingEntity)getDead().getEntity().getLastDamageCause().getEntity()).getHealth();
double damageDone = getEntity().getLastDamageCause().getFinalDamage();
- double damageLeft = Math.abs(healthBefore - damageDone);
double maxHealth = getEntity().getAttribute(Attribute.GENERIC_MAX_HEALTH).getValue();
+ if (getDead().getEntityConfig().isTraitEnabled(Potion.class.getAnnotation(TraitMetadata.class).path())) {
+ if (getDead().getEntity().getPotionEffect(PotionEffectType.HEALTH_BOOST) != null) {
+ maxHealth = getEntity().getAttribute(Attribute.GENERIC_MAX_HEALTH).getBaseValue();
+ }
+ }
+ double damageLeft = Math.min(maxHealth * (getDead().getSize() - 1), Math.abs(healthBefore - damageDone));
double divided = damageLeft / maxHealth;
int entities = (int) Math.floor(divided);
leftOverDamage = (divided - entities) * maxHealth;
@@ -28,15 +41,30 @@ public int calculateStep() {
@Override
public void onSpawn(StackEntity spawned) {
- double maxHealth = getEntity().getAttribute(Attribute.GENERIC_MAX_HEALTH).getValue();
+ AttributeInstance attribute = getEntity().getAttribute(Attribute.GENERIC_MAX_HEALTH);
+ AttributeInstance spawnedAttribute = spawned.getEntity().getAttribute(Attribute.GENERIC_MAX_HEALTH);
+ double maxHealth = Math.min(attribute.getValue(), attribute.getDefaultValue());
StackableMobHook smh = sm.getHookManager().getApplicableHook(spawned);
- if (smh instanceof MythicMobsHook) {
- sm.getServer().getScheduler().runTaskLater(sm, bukkitTask -> {
+ if (smh instanceof MythicMobsStackHook) {
+ sm.getScheduler().runTaskLater(spawned.getEntity(), () -> {
if (!spawned.getEntity().isDead()) {
spawned.getEntity().setHealth(maxHealth - leftOverDamage);
}
}, 5);
}
- spawned.getEntity().setHealth(maxHealth - leftOverDamage);
+ try {
+ spawned.getEntity().setHealth(maxHealth - leftOverDamage);
+ } catch (IllegalArgumentException e) {
+ if (getStackMob().isStepDamageError()) {
+ return;
+ }
+ sm.getLogger().warning("New health value is too high! Please report and include the message below.");
+ sm.getLogger().info(attribute.getBaseValue() + "," + attribute.getDefaultValue() + "," + attribute.getValue() + "," + leftOverDamage);
+ sm.getLogger().info("Type: " + getEntity().getType() + ", Name: " + getEntity().getCustomName() + ", Location: " + getEntity().getLocation());
+ if (spawnedAttribute != null) {
+ sm.getLogger().info(spawnedAttribute.getBaseValue() + "," + spawnedAttribute.getDefaultValue() + "," + spawnedAttribute.getValue());
+ }
+ getStackMob().setStepDamageError(true);
+ }
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/tags/DisplayTag.java b/src/main/java/uk/antiperson/stackmob/entity/tags/DisplayTag.java
new file mode 100644
index 00000000..d9947d43
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/entity/tags/DisplayTag.java
@@ -0,0 +1,69 @@
+package uk.antiperson.stackmob.entity.tags;
+
+import net.kyori.adventure.text.Component;
+import org.bukkit.Location;
+import org.bukkit.entity.Display;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.TextDisplay;
+import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.entity.StackEntity;
+
+public class DisplayTag {
+
+ private final StackEntity stackEntity;
+ private TextDisplay display;
+ private final StackMob sm;
+
+ public DisplayTag(StackMob sm, StackEntity stackEntity) {
+ this.stackEntity = stackEntity;
+ this.sm = sm;
+ }
+
+ private StackEntity getStackEntity() {
+ return stackEntity;
+ }
+
+ public void spawn() {
+ display = (TextDisplay) stackEntity.getWorld().spawnEntity(calculateLocation(stackEntity.getEntity().getLocation()), EntityType.TEXT_DISPLAY);
+ display.setBillboard(Display.Billboard.CENTER);
+ display.setTeleportDuration(1);
+ if (getStackEntity().getEntityConfig().getTagMode() == StackEntity.TagMode.NEARBY) {
+ float multiplier = stackEntity.getEntityConfig().getTagNearbyRadius() / readTrackingRange();
+ display.setViewRange(multiplier);
+ }
+ display.setPersistent(false);
+ updateName(stackEntity.getTag().getDisplayName());
+ }
+
+ private float readTrackingRange() {
+ String path = sm.getServer().spigot().getSpigotConfig().isConfigurationSection("world-settings." + getStackEntity().getWorld().getName()) ?
+ getStackEntity().getWorld().getName() : "default";
+ String finalPath = "world-settings." + path + ".entity-tracking-range.display";
+ return sm.getServer().spigot().getSpigotConfig().getInt(finalPath);
+ }
+
+ public void remove() {
+ display.remove();
+ }
+
+ public boolean exists() {
+ return display != null;
+ }
+
+ public void move(Location location) {
+ display.teleportAsync(calculateLocation(location));
+ }
+
+ private Location calculateLocation(Location location) {
+ double adjustment = stackEntity.getEntity().customName() == null ? 0.3 : 0.5;
+ if (stackEntity.getEntityConfig().getArmorstandOffset() > 0) {
+ adjustment = stackEntity.getEntityConfig().getArmorstandOffset();
+ }
+ return location.add(0, stackEntity.getEntity().getHeight() + adjustment, 0);
+ }
+
+ public void updateName(Component name) {
+ display.text(name);
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/tags/DisplayTagListeners.java b/src/main/java/uk/antiperson/stackmob/entity/tags/DisplayTagListeners.java
new file mode 100644
index 00000000..39ca4703
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/entity/tags/DisplayTagListeners.java
@@ -0,0 +1,53 @@
+package uk.antiperson.stackmob.entity.tags;
+
+import io.papermc.paper.event.entity.EntityMoveEvent;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.entity.Mob;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityRemoveEvent;
+import org.bukkit.event.player.PlayerMoveEvent;
+import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.entity.StackEntity;
+
+public class DisplayTagListeners implements Listener {
+
+ private final StackMob sm;
+ public DisplayTagListeners(StackMob sm) {
+ this.sm = sm;
+ }
+
+ @EventHandler
+ public void onEntityMove(EntityMoveEvent event) {
+ if (!event.hasChangedPosition()) {
+ return;
+ }
+ StackEntity stackEntity = sm.getEntityManager().getStackEntity(event.getEntity());
+ if (stackEntity == null) {
+ return;
+ }
+ if (stackEntity.getDisplayTag().exists()) {
+ stackEntity.getDisplayTag().move(event.getTo().clone());
+ return;
+ }
+ stackEntity.getTag().update();
+ }
+
+ @EventHandler
+ public void onEntityRemove(EntityRemoveEvent event) {
+ if (!(event.getEntity() instanceof LivingEntity)) {
+ return;
+ }
+ if (event.getCause() == EntityRemoveEvent.Cause.UNLOAD) {
+ return;
+ }
+ StackEntity stackEntity = sm.getEntityManager().getStackEntity((LivingEntity) event.getEntity());
+ if (stackEntity == null) {
+ return;
+ }
+ if (stackEntity.getDisplayTag().exists()) {
+ stackEntity.getDisplayTag().remove();
+ }
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/Trait.java b/src/main/java/uk/antiperson/stackmob/entity/traits/Trait.java
index 506a52c3..2caaac4b 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/Trait.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/Trait.java
@@ -2,7 +2,7 @@
import org.bukkit.entity.LivingEntity;
-public interface Trait {
+public interface Trait {
/**
* Check if two entities have the same entity specific traits (eg. sheep colour, villager profession)
@@ -10,12 +10,12 @@ public interface Trait {
* @param nearby the entity the first should stack with
* @return whether these two entities should stack.
*/
- boolean checkTrait(LivingEntity first, LivingEntity nearby);
+ boolean checkTrait(T first, T nearby);
/**
* Copy the traits of the dead entity to that of the newly spawned entity.
* @param dead the entity that died.
* @param spawned the entity that was spawned to replace it.
*/
- void applyTrait(LivingEntity spawned, LivingEntity dead);
+ void applyTrait(T spawned, T dead);
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/TraitManager.java b/src/main/java/uk/antiperson/stackmob/entity/traits/TraitManager.java
index 2caea6f9..f3645c80 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/TraitManager.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/TraitManager.java
@@ -1,24 +1,29 @@
package uk.antiperson.stackmob.entity.traits;
+import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
-import org.bukkit.entity.Piglin;
import uk.antiperson.stackmob.StackMob;
import uk.antiperson.stackmob.entity.StackEntity;
import uk.antiperson.stackmob.entity.traits.trait.*;
import uk.antiperson.stackmob.utils.Utilities;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.ParameterizedType;
+import java.util.EnumMap;
import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
public class TraitManager {
- private final HashSet traits;
+ private final Map>> traits;
private final StackMob sm;
public TraitManager(StackMob sm) {
this.sm = sm;
- this.traits = new HashSet<>();
+ this.traits = new EnumMap<>(EntityType.class);
}
- public void registerTraits() throws InstantiationException, IllegalAccessException {
+ public void registerTraits() throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
registerTrait(SheepColor.class);
registerTrait(SheepShear.class);
registerTrait(HorseColor.class);
@@ -35,25 +40,41 @@ public void registerTraits() throws InstantiationException, IllegalAccessExcepti
registerTrait(ZombieBaby.class);
registerTrait(BeeNectar.class);
registerTrait(BeeStung.class);
- if (!Utilities.isNewBukkit()) {
- return;
+ registerTrait(Leash.class);
+ registerTrait(Potion.class);
+ registerTrait(VillagerProfession.class);
+ if (Utilities.isPaper()) {
+ registerTrait(TurtleHasEgg.class);
}
registerTrait(ZoglinBaby.class);
registerTrait(PiglinBaby.class);
+ if (Utilities.isVersionAtLeast(Utilities.MinecraftVersion.V1_19_4)) {
+ registerTrait(FrogVariant.class);
+ registerTrait(AllayOwner.class);
+ }
}
/**
* If a class hasn't been disabled in the config, add this to the hashset so it can be looped over.
*
- * TODO: Perhaps there could be a hashset which contains a list of entity types that should be checked.
- * @param trait class that implements trait
+ * @param traitClass class that implements trait
* @throws IllegalAccessException if class is not accessible
* @throws InstantiationException if class can not be instantiated
+ * @throws NoSuchMethodException if class constructor can not be found
+ * @throws InvocationTargetException if instantiation fails
*/
- private void registerTrait(Class extends Trait> trait) throws IllegalAccessException, InstantiationException {
- TraitMetadata traitMetadata = trait.getAnnotation(TraitMetadata.class);
- if (sm.getMainConfig().isTraitEnabled(traitMetadata.path()) || sm.getMainConfig().getBoolean(traitMetadata.path())) {
- traits.add(trait.newInstance());
+ private > void registerTrait(Class traitClass) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
+ final TraitMetadata traitMetadata = traitClass.getAnnotation(TraitMetadata.class);
+ if (!sm.getMainConfig().getConfig().isTraitEnabled(traitMetadata.path())) {
+ return;
+ }
+ Trait trait = (Trait) traitClass.getDeclaredConstructor().newInstance();
+ ParameterizedType parameterizedType = (ParameterizedType) trait.getClass().getGenericInterfaces()[0];
+ Class extends LivingEntity> typeArgument = (Class extends LivingEntity>) parameterizedType.getActualTypeArguments()[0];
+ for (EntityType entityType : EntityType.values()) {
+ if (entityType.getEntityClass() != null && typeArgument.isAssignableFrom(entityType.getEntityClass())) {
+ traits.computeIfAbsent(entityType, type -> new HashSet<>()).add(trait);
+ }
}
}
@@ -64,11 +85,13 @@ private void registerTrait(Class extends Trait> trait) throws IllegalAccessExc
* @return if these entities have any not matching characteristics (traits.)
*/
public boolean checkTraits(StackEntity first, StackEntity nearby) {
- for (Trait trait : traits) {
- if (isTraitApplicable(trait, first.getEntity())) {
- if (trait.checkTrait(first.getEntity(), nearby.getEntity())) {
- return true;
- }
+ Set> set = traits.get(first.getEntity().getType());
+ if (set == null) {
+ return false;
+ }
+ for (Trait trait : set) {
+ if (trait.checkTrait(first.getEntity(), nearby.getEntity())) {
+ return true;
}
}
return false;
@@ -80,21 +103,12 @@ public boolean checkTraits(StackEntity first, StackEntity nearby) {
* @param dead the entity which traits should be copied from.
*/
public void applyTraits(StackEntity spawned, StackEntity dead) {
- for (Trait trait : traits) {
- if (isTraitApplicable(trait, spawned.getEntity())) {
- trait.applyTrait(spawned.getEntity(), dead.getEntity());
- }
+ Set> set = traits.get(spawned.getEntity().getType());
+ if (set == null) {
+ return;
+ }
+ for (Trait trait : set) {
+ trait.applyTrait(spawned.getEntity(), dead.getEntity());
}
- }
-
- /**
- * Check if the trait is applicable to the given entity.
- * @param trait the trait to check.
- * @param entity the entity to check.
- * @return if the trait is applicable to the given entity.
- */
- private boolean isTraitApplicable(Trait trait, LivingEntity entity) {
- TraitMetadata traitMetadata = trait.getClass().getAnnotation(TraitMetadata.class);
- return traitMetadata.entity().isAssignableFrom(entity.getClass());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/TraitMetadata.java b/src/main/java/uk/antiperson/stackmob/entity/traits/TraitMetadata.java
index a2f32eda..e4f1278e 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/TraitMetadata.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/TraitMetadata.java
@@ -1,13 +1,10 @@
package uk.antiperson.stackmob.entity.traits;
-import org.bukkit.entity.Mob;
-
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface TraitMetadata {
- Class extends Mob> entity();
String path();
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/Age.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/Age.java
index 0c05f799..6ffdb67d 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/Age.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/Age.java
@@ -1,20 +1,19 @@
package uk.antiperson.stackmob.entity.traits.trait;
import org.bukkit.entity.Ageable;
-import org.bukkit.entity.LivingEntity;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Ageable.class, path = "age")
-public class Age implements Trait {
+@TraitMetadata(path = "age")
+public class Age implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((Ageable) first).isAdult() != ((Ageable) nearby).isAdult();
+ public boolean checkTrait(Ageable first, Ageable nearby) {
+ return first.isAdult() != nearby.isAdult();
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Ageable) spawned).setAge(((Ageable) dead).getAge());
+ public void applyTrait(Ageable spawned, Ageable dead) {
+ spawned.setAge(dead.getAge());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/AllayOwner.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/AllayOwner.java
new file mode 100644
index 00000000..095783c5
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/AllayOwner.java
@@ -0,0 +1,20 @@
+package uk.antiperson.stackmob.entity.traits.trait;
+
+import org.bukkit.entity.Allay;
+import org.bukkit.entity.memory.MemoryKey;
+import uk.antiperson.stackmob.entity.traits.Trait;
+import uk.antiperson.stackmob.entity.traits.TraitMetadata;
+
+@TraitMetadata(path = "allay-owner")
+public class AllayOwner implements Trait {
+
+ @Override
+ public boolean checkTrait(Allay first, Allay nearby) {
+ return first.getMemory(MemoryKey.LIKED_PLAYER) != nearby.getMemory(MemoryKey.LIKED_PLAYER);
+ }
+
+ @Override
+ public void applyTrait(Allay spawned, Allay dead) {
+ spawned.setMemory(MemoryKey.LIKED_PLAYER, dead.getMemory(MemoryKey.LIKED_PLAYER));
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/BeeNectar.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/BeeNectar.java
index c9b07713..ed53c37d 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/BeeNectar.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/BeeNectar.java
@@ -1,19 +1,18 @@
package uk.antiperson.stackmob.entity.traits.trait;
import org.bukkit.entity.Bee;
-import org.bukkit.entity.LivingEntity;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Bee.class, path = "bee-nectar")
-public class BeeNectar implements Trait {
+@TraitMetadata(path = "bee-nectar")
+public class BeeNectar implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((Bee) first).hasNectar() != ((Bee) nearby).hasNectar();
+ public boolean checkTrait(Bee first, Bee nearby) {
+ return first.hasNectar() != nearby.hasNectar();
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Bee) spawned).setHasNectar(((Bee) dead).hasNectar());
+ public void applyTrait(Bee spawned, Bee dead) {
+ spawned.setHasNectar(spawned.hasNectar());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/BeeStung.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/BeeStung.java
index d950e608..31dbddc7 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/BeeStung.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/BeeStung.java
@@ -1,20 +1,19 @@
package uk.antiperson.stackmob.entity.traits.trait;
import org.bukkit.entity.Bee;
-import org.bukkit.entity.LivingEntity;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Bee.class, path = "bee-stung")
-public class BeeStung implements Trait {
+@TraitMetadata(path = "bee-stung")
+public class BeeStung implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((Bee) first).hasStung() != ((Bee) nearby).hasStung();
+ public boolean checkTrait(Bee first, Bee nearby) {
+ return first.hasStung() != nearby.hasStung();
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Bee) spawned).setHasStung(((Bee) dead).hasStung());
+ public void applyTrait(Bee spawned, Bee dead) {
+ spawned.setHasStung(dead.hasStung());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/BreedMode.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/BreedMode.java
index 5e3ef356..918cda0e 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/BreedMode.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/BreedMode.java
@@ -1,20 +1,19 @@
package uk.antiperson.stackmob.entity.traits.trait;
import org.bukkit.entity.Animals;
-import org.bukkit.entity.LivingEntity;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Animals.class, path = "breed-mode")
-public class BreedMode implements Trait {
+@TraitMetadata(path = "breed-mode")
+public class BreedMode implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((Animals) first).canBreed() != ((Animals) nearby).canBreed();
+ public boolean checkTrait(Animals first, Animals nearby) {
+ return first.canBreed() != nearby.canBreed();
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Animals) spawned).setBreed(((Animals) dead).canBreed());
+ public void applyTrait(Animals spawned, Animals dead) {
+ spawned.setBreed(dead.canBreed());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/CatType.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/CatType.java
index 4c2a791c..3b09ca14 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/CatType.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/CatType.java
@@ -2,19 +2,20 @@
import org.bukkit.entity.Cat;
import org.bukkit.entity.LivingEntity;
+import org.checkerframework.checker.units.qual.C;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Cat.class, path = "cat-type")
-public class CatType implements Trait {
+@TraitMetadata(path = "cat-type")
+public class CatType implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((Cat) first).getCatType() != ((Cat) nearby).getCatType();
+ public boolean checkTrait(Cat first, Cat nearby) {
+ return first.getCatType() != nearby.getCatType();
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Cat) spawned).setCatType(((Cat) dead).getCatType());
+ public void applyTrait(Cat spawned, Cat dead) {
+ spawned.setCatType(dead.getCatType());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/DrownedItem.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/DrownedItem.java
index 35961d75..ae695f15 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/DrownedItem.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/DrownedItem.java
@@ -1,47 +1,37 @@
package uk.antiperson.stackmob.entity.traits.trait;
-import org.bukkit.Material;
import org.bukkit.entity.Drowned;
-import org.bukkit.entity.LivingEntity;
+import org.bukkit.inventory.EquipmentSlot;
+import org.bukkit.inventory.ItemStack;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
+import uk.antiperson.stackmob.utils.Utilities;
-import java.util.Arrays;
-import java.util.List;
-
-@TraitMetadata(entity = Drowned.class, path = "drowned-hand-item")
-public class DrownedItem implements Trait {
-
- private final List materials = Arrays.asList(Material.NAUTILUS_SHELL, Material.TRIDENT);
-
+@TraitMetadata(path = "drowned-hand-item")
+public class DrownedItem implements Trait {
+
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- Drowned oriDrowned = (Drowned) first;
- Drowned nearDrowned = (Drowned) nearby;
- if(materials.contains(oriDrowned.getEquipment().getItemInMainHand().getType()) ||
- materials.contains(nearDrowned.getEquipment().getItemInMainHand().getType())){
- if(oriDrowned.getEquipment().getItemInMainHand().getType() !=
- nearDrowned.getEquipment().getItemInMainHand().getType()){
+ public boolean checkTrait (Drowned first, Drowned nearby) {
+ for (EquipmentSlot equipmentSlot : Utilities.HAND_SLOTS) {
+ ItemStack oriItemStack = first.getEquipment().getItem(equipmentSlot);
+ ItemStack nearItemStack = nearby.getEquipment().getItem(equipmentSlot);
+ if (!oriItemStack.isSimilar(nearItemStack)) {
+ continue;
+ }
+ if (Utilities.DROWNED_MATERIALS.contains(oriItemStack.getType())) {
return true;
}
}
- if(materials.contains(oriDrowned.getEquipment().getItemInOffHand().getType()) ||
- materials.contains(nearDrowned.getEquipment().getItemInOffHand().getType())){
- return oriDrowned.getEquipment().getItemInOffHand().getType() !=
- nearDrowned.getEquipment().getItemInOffHand().getType();
- }
return false;
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- Drowned oriDrowned = (Drowned) dead;
- Drowned spawnDrowned = (Drowned) spawned;
- if(materials.contains(oriDrowned.getEquipment().getItemInMainHand().getType())){
- spawnDrowned.getEquipment().setItemInMainHand(oriDrowned.getEquipment().getItemInMainHand());
- }
- if(materials.contains(oriDrowned.getEquipment().getItemInOffHand().getType())){
- spawnDrowned.getEquipment().setItemInOffHand(oriDrowned.getEquipment().getItemInOffHand());
+ public void applyTrait(Drowned spawned, Drowned dead) {
+ for (EquipmentSlot equipmentSlot : Utilities.HAND_SLOTS) {
+ ItemStack item = dead.getEquipment().getItem(equipmentSlot);
+ if (Utilities.DROWNED_MATERIALS.contains(item.getType())) {
+ spawned.getEquipment().setItem(equipmentSlot, item);
+ }
}
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/FoxType.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/FoxType.java
index bdfe62d0..5631b743 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/FoxType.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/FoxType.java
@@ -1,20 +1,19 @@
package uk.antiperson.stackmob.entity.traits.trait;
import org.bukkit.entity.Fox;
-import org.bukkit.entity.LivingEntity;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Fox.class, path = "fox-type")
-public class FoxType implements Trait {
+@TraitMetadata(path = "fox-type")
+public class FoxType implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((Fox) first).getFoxType() != ((Fox) nearby).getFoxType();
+ public boolean checkTrait(Fox first, Fox nearby) {
+ return first.getFoxType() != nearby.getFoxType();
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Fox) spawned).setFoxType(((Fox) dead).getFoxType());
+ public void applyTrait(Fox spawned, Fox dead) {
+ spawned.setFoxType(dead.getFoxType());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/FrogVariant.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/FrogVariant.java
new file mode 100644
index 00000000..3890d127
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/FrogVariant.java
@@ -0,0 +1,20 @@
+package uk.antiperson.stackmob.entity.traits.trait;
+
+import org.bukkit.entity.Frog;
+import org.bukkit.entity.LivingEntity;
+import uk.antiperson.stackmob.entity.traits.Trait;
+import uk.antiperson.stackmob.entity.traits.TraitMetadata;
+
+@TraitMetadata(path = "frog-variant")
+public class FrogVariant implements Trait {
+
+ @Override
+ public boolean checkTrait(Frog first, Frog nearby) {
+ return first.getVariant() != nearby.getVariant();
+ }
+
+ @Override
+ public void applyTrait(Frog spawned, Frog dead) {
+ spawned.setVariant(dead.getVariant());
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/HorseColor.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/HorseColor.java
index 6f1cffa9..3e9300ab 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/HorseColor.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/HorseColor.java
@@ -1,22 +1,21 @@
package uk.antiperson.stackmob.entity.traits.trait;
import org.bukkit.entity.Horse;
-import org.bukkit.entity.LivingEntity;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Horse.class, path = "horse-color")
-public class HorseColor implements Trait {
+@TraitMetadata(path = "horse-color")
+public class HorseColor implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return (((Horse) first).getColor() != ((Horse) nearby).getColor()) ||
- (((Horse) first).getStyle() != ((Horse) nearby).getStyle());
+ public boolean checkTrait(Horse first, Horse nearby) {
+ return (first.getColor() != nearby.getColor()) ||
+ (first.getStyle() != nearby.getStyle());
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Horse) spawned).setColor(((Horse) dead).getColor());
- ((Horse) spawned).setStyle(((Horse) spawned).getStyle());
+ public void applyTrait(Horse spawned, Horse dead) {
+ spawned.setColor(dead.getColor());
+ spawned.setStyle(spawned.getStyle());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/Leash.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/Leash.java
new file mode 100644
index 00000000..292d6918
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/Leash.java
@@ -0,0 +1,25 @@
+package uk.antiperson.stackmob.entity.traits.trait;
+
+import org.bukkit.entity.Mob;
+import uk.antiperson.stackmob.entity.traits.Trait;
+import uk.antiperson.stackmob.entity.traits.TraitMetadata;
+
+@TraitMetadata(path = "leashed")
+public class Leash implements Trait {
+
+ @Override
+ public boolean checkTrait(Mob first, Mob nearby) {
+ if (first.isLeashed() != nearby.isLeashed()) {
+ return true;
+ }
+ return first.isLeashed() && first.getLeashHolder() != nearby.getLeashHolder();
+ }
+
+ @Override
+ public void applyTrait(Mob spawned, Mob dead) {
+ if (!dead.isLeashed()) {
+ return;
+ }
+ spawned.setLeashHolder(dead.getLeashHolder());
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/LlamaColor.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/LlamaColor.java
index 7c7d9aa9..06be3e27 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/LlamaColor.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/LlamaColor.java
@@ -1,20 +1,19 @@
package uk.antiperson.stackmob.entity.traits.trait;
-import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Llama;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Llama.class, path = "llama-color")
-public class LlamaColor implements Trait {
+@TraitMetadata(path = "llama-color")
+public class LlamaColor implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((Llama) first).getColor() != ((Llama) nearby).getColor();
+ public boolean checkTrait(Llama first, Llama nearby) {
+ return first.getColor() != nearby.getColor();
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Llama) spawned).setColor(((Llama) dead).getColor());
+ public void applyTrait(Llama spawned, Llama dead) {
+ spawned.setColor(dead.getColor());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/LoveMode.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/LoveMode.java
index df48bb97..88a170ed 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/LoveMode.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/LoveMode.java
@@ -1,20 +1,19 @@
package uk.antiperson.stackmob.entity.traits.trait;
import org.bukkit.entity.Animals;
-import org.bukkit.entity.LivingEntity;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Animals.class, path = "love-mode")
-public class LoveMode implements Trait {
+@TraitMetadata(path = "love-mode")
+public class LoveMode implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((Animals) first).getLoveModeTicks() != 0 || ((Animals) nearby).getLoveModeTicks() != 0;
+ public boolean checkTrait(Animals first, Animals nearby) {
+ return first.getLoveModeTicks() != 0 || nearby.getLoveModeTicks() != 0;
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Animals) spawned).setLoveModeTicks(((Animals) spawned).getLoveModeTicks());
+ public void applyTrait(Animals spawned, Animals dead) {
+ spawned.setLoveModeTicks(spawned.getLoveModeTicks());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/MooshroomVariant.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/MooshroomVariant.java
index dec04d11..09281ca6 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/MooshroomVariant.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/MooshroomVariant.java
@@ -1,20 +1,19 @@
package uk.antiperson.stackmob.entity.traits.trait;
-import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.MushroomCow;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = MushroomCow.class, path = "mooshroom-variant")
-public class MooshroomVariant implements Trait {
+@TraitMetadata(path = "mooshroom-variant")
+public class MooshroomVariant implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((MushroomCow) first).getVariant() != ((MushroomCow) nearby).getVariant();
+ public boolean checkTrait(MushroomCow first, MushroomCow nearby) {
+ return first.getVariant() != nearby.getVariant();
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((MushroomCow) spawned).setVariant(((MushroomCow) dead).getVariant());
+ public void applyTrait(MushroomCow spawned, MushroomCow dead) {
+ spawned.setVariant(dead.getVariant());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/ParrotVariant.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/ParrotVariant.java
index 39f8934c..3839af96 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/ParrotVariant.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/ParrotVariant.java
@@ -1,20 +1,19 @@
package uk.antiperson.stackmob.entity.traits.trait;
-import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Parrot;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Parrot.class, path = "parrot-variant")
-public class ParrotVariant implements Trait {
+@TraitMetadata(path = "parrot-variant")
+public class ParrotVariant implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((Parrot) first).getVariant() != ((Parrot) nearby).getVariant();
+ public boolean checkTrait(Parrot first, Parrot nearby) {
+ return first.getVariant() != nearby.getVariant();
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Parrot) spawned).setVariant(((Parrot) spawned).getVariant());
+ public void applyTrait(Parrot spawned, Parrot dead) {
+ spawned.setVariant(spawned.getVariant());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/PiglinBaby.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/PiglinBaby.java
index 4f5bc61e..5fbfc42f 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/PiglinBaby.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/PiglinBaby.java
@@ -1,20 +1,19 @@
package uk.antiperson.stackmob.entity.traits.trait;
-import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Piglin;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Piglin.class, path = "piglin-baby")
-public class PiglinBaby implements Trait {
+@TraitMetadata(path = "piglin-baby")
+public class PiglinBaby implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((Piglin) first).isBaby() != ((Piglin) nearby).isBaby();
+ public boolean checkTrait(Piglin first, Piglin nearby) {
+ return first.isBaby() != nearby.isBaby();
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Piglin) spawned).setBaby(((Piglin) dead).isBaby());
+ public void applyTrait(Piglin spawned, Piglin dead) {
+ spawned.setBaby(dead.isBaby());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/Potion.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/Potion.java
new file mode 100644
index 00000000..e4d43961
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/Potion.java
@@ -0,0 +1,22 @@
+package uk.antiperson.stackmob.entity.traits.trait;
+
+import org.bukkit.entity.Mob;
+import org.bukkit.potion.PotionEffect;
+import uk.antiperson.stackmob.entity.traits.Trait;
+import uk.antiperson.stackmob.entity.traits.TraitMetadata;
+
+@TraitMetadata(path = "potion-effect")
+public class Potion implements Trait {
+
+ @Override
+ public boolean checkTrait(Mob first, Mob nearby) {
+ return false;
+ }
+
+ @Override
+ public void applyTrait(Mob spawned, Mob dead) {
+ for (PotionEffect potionEffect : dead.getActivePotionEffects()) {
+ spawned.addPotionEffect(potionEffect);
+ }
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/SheepColor.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/SheepColor.java
index 6d7779a8..9d338e51 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/SheepColor.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/SheepColor.java
@@ -1,20 +1,19 @@
package uk.antiperson.stackmob.entity.traits.trait;
-import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Sheep;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Sheep.class, path = "sheep-color")
-public class SheepColor implements Trait {
+@TraitMetadata(path = "sheep-color")
+public class SheepColor implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((Sheep) first).getColor() != ((Sheep) nearby).getColor();
+ public boolean checkTrait(Sheep first, Sheep nearby) {
+ return first.getColor() != nearby.getColor();
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Sheep) spawned).setColor(((Sheep) dead).getColor());
+ public void applyTrait(Sheep spawned, Sheep dead) {
+ spawned.setColor(dead.getColor());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/SheepShear.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/SheepShear.java
index 5ae2f3ad..32e31055 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/SheepShear.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/SheepShear.java
@@ -1,20 +1,19 @@
package uk.antiperson.stackmob.entity.traits.trait;
-import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Sheep;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Sheep.class, path = "sheep-sheared")
-public class SheepShear implements Trait {
+@TraitMetadata(path = "sheep-sheared")
+public class SheepShear implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((Sheep) first).isSheared() != ((Sheep) nearby).isSheared();
+ public boolean checkTrait(Sheep first, Sheep nearby) {
+ return first.isSheared() != nearby.isSheared();
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Sheep) spawned).setSheared(((Sheep) dead).isSheared());
+ public void applyTrait(Sheep spawned, Sheep dead) {
+ spawned.setSheared(dead.isSheared());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/SlimeSize.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/SlimeSize.java
index 0a9ac1c1..758045f9 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/SlimeSize.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/SlimeSize.java
@@ -1,20 +1,19 @@
package uk.antiperson.stackmob.entity.traits.trait;
-import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Slime;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Slime.class, path = "slime-size")
-public class SlimeSize implements Trait {
+@TraitMetadata(path = "slime-size")
+public class SlimeSize implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((Slime) first).getSize() != ((Slime) nearby).getSize();
+ public boolean checkTrait(Slime first, Slime nearby) {
+ return first.getSize() != nearby.getSize();
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Slime) spawned).setSize(((Slime) dead).getSize());
+ public void applyTrait(Slime spawned, Slime dead) {
+ spawned.setSize(dead.getSize());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/TurtleHasEgg.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/TurtleHasEgg.java
new file mode 100644
index 00000000..c6c34c79
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/TurtleHasEgg.java
@@ -0,0 +1,19 @@
+package uk.antiperson.stackmob.entity.traits.trait;
+
+import org.bukkit.entity.Turtle;
+import uk.antiperson.stackmob.entity.traits.Trait;
+import uk.antiperson.stackmob.entity.traits.TraitMetadata;
+
+@TraitMetadata(path = "has-egg")
+public class TurtleHasEgg implements Trait {
+
+ @Override
+ public boolean checkTrait(Turtle first, Turtle nearby) {
+ return first.hasEgg() || nearby.hasEgg();
+ }
+
+ @Override
+ public void applyTrait(Turtle spawned, Turtle dead) {
+ spawned.setHasEgg(spawned.hasEgg());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/VillagerProfession.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/VillagerProfession.java
new file mode 100644
index 00000000..ec720efd
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/VillagerProfession.java
@@ -0,0 +1,19 @@
+package uk.antiperson.stackmob.entity.traits.trait;
+
+import org.bukkit.entity.Villager;
+import uk.antiperson.stackmob.entity.traits.Trait;
+import uk.antiperson.stackmob.entity.traits.TraitMetadata;
+
+@TraitMetadata(path = "villager-profession")
+public class VillagerProfession implements Trait {
+
+ @Override
+ public boolean checkTrait(Villager first, Villager nearby) {
+ return first.getProfession() != nearby.getProfession();
+ }
+
+ @Override
+ public void applyTrait(Villager spawned, Villager dead) {
+ spawned.setProfession(dead.getProfession());
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/ZoglinBaby.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/ZoglinBaby.java
index 7cd3ff2b..3a56b634 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/ZoglinBaby.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/ZoglinBaby.java
@@ -1,20 +1,19 @@
package uk.antiperson.stackmob.entity.traits.trait;
-import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Zoglin;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Zoglin.class, path = "zoglin-baby")
-public class ZoglinBaby implements Trait {
+@TraitMetadata(path = "zoglin-baby")
+public class ZoglinBaby implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((Zoglin) first).isBaby() != ((Zoglin) nearby).isBaby();
+ public boolean checkTrait(Zoglin first, Zoglin nearby) {
+ return first.isBaby() != nearby.isBaby();
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Zoglin) spawned).setBaby(((Zoglin) dead).isBaby());
+ public void applyTrait(Zoglin spawned, Zoglin dead) {
+ spawned.setBaby(dead.isBaby());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/ZombieBaby.java b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/ZombieBaby.java
index 70ccfff7..bc25eb92 100644
--- a/src/main/java/uk/antiperson/stackmob/entity/traits/trait/ZombieBaby.java
+++ b/src/main/java/uk/antiperson/stackmob/entity/traits/trait/ZombieBaby.java
@@ -1,20 +1,19 @@
package uk.antiperson.stackmob.entity.traits.trait;
-import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Zombie;
import uk.antiperson.stackmob.entity.traits.Trait;
import uk.antiperson.stackmob.entity.traits.TraitMetadata;
-@TraitMetadata(entity = Zombie.class, path = "age")
-public class ZombieBaby implements Trait {
+@TraitMetadata(path = "age")
+public class ZombieBaby implements Trait {
@Override
- public boolean checkTrait(LivingEntity first, LivingEntity nearby) {
- return ((Zombie) first).isBaby() != ((Zombie) nearby).isBaby();
+ public boolean checkTrait(Zombie first, Zombie nearby) {
+ return first.isBaby() != nearby.isBaby();
}
@Override
- public void applyTrait(LivingEntity spawned, LivingEntity dead) {
- ((Zombie) spawned).setBaby(((Zombie) dead).isBaby());
+ public void applyTrait(Zombie spawned, Zombie dead) {
+ spawned.setBaby(dead.isBaby());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/events/EventHelper.java b/src/main/java/uk/antiperson/stackmob/events/EventHelper.java
index b242e079..b52677f7 100644
--- a/src/main/java/uk/antiperson/stackmob/events/EventHelper.java
+++ b/src/main/java/uk/antiperson/stackmob/events/EventHelper.java
@@ -1,10 +1,25 @@
package uk.antiperson.stackmob.events;
import org.bukkit.Bukkit;
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.event.entity.EntityDeathEvent;
+import org.bukkit.inventory.ItemStack;
import uk.antiperson.stackmob.entity.StackEntity;
+import java.util.Map;
+
public class EventHelper {
+ public static StackDropItemEvent callStackDropItemEvent(
+ StackEntity entity,
+ Map drops,
+ EntityDeathEvent originalEvent
+ ) {
+ StackDropItemEvent event = new StackDropItemEvent(entity, drops, originalEvent);
+ Bukkit.getPluginManager().callEvent(event);
+ return event;
+ }
+
public static StackMergeEvent callStackMergeEvent(StackEntity first, StackEntity nearby) {
StackMergeEvent event = new StackMergeEvent(first, nearby);
Bukkit.getPluginManager().callEvent(event);
@@ -16,4 +31,10 @@ public static StackDeathEvent callStackDeathEvent(StackEntity dead, int deathSte
Bukkit.getPluginManager().callEvent(event);
return event;
}
+
+ public static StackSpawnEvent callStackSpawnEvent(LivingEntity spawned) {
+ StackSpawnEvent event = new StackSpawnEvent(spawned);
+ Bukkit.getPluginManager().callEvent(event);
+ return event;
+ }
}
diff --git a/src/main/java/uk/antiperson/stackmob/events/StackDeathEvent.java b/src/main/java/uk/antiperson/stackmob/events/StackDeathEvent.java
index d1d46146..e7ec76cf 100644
--- a/src/main/java/uk/antiperson/stackmob/events/StackDeathEvent.java
+++ b/src/main/java/uk/antiperson/stackmob/events/StackDeathEvent.java
@@ -9,7 +9,7 @@
public class StackDeathEvent extends StackEvent {
private static final HandlerList handlers = new HandlerList();
- private final int deathStep;
+ private int deathStep;
public StackDeathEvent(StackEntity stackEntity, int deathStep) {
super(stackEntity);
@@ -31,4 +31,17 @@ public static HandlerList getHandlerList() {
public int getDeathStep() {
return deathStep;
}
+
+ /**
+ * Set the amount of entities to be removed from the stack.
+ */
+ public void setDeathStep(int deathStep) {
+ if (deathStep < 0) {
+ throw new IllegalArgumentException("Death step must be greater than or equal to one.");
+ }
+ if (deathStep > getStackEntity().getSize()) {
+ throw new IllegalArgumentException("Death step cannot be greater than the size of the killed entity!");
+ }
+ this.deathStep = deathStep;
+ }
}
diff --git a/src/main/java/uk/antiperson/stackmob/events/StackDropItemEvent.java b/src/main/java/uk/antiperson/stackmob/events/StackDropItemEvent.java
new file mode 100644
index 00000000..da663952
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/events/StackDropItemEvent.java
@@ -0,0 +1,53 @@
+package uk.antiperson.stackmob.events;
+
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.entity.EntityDeathEvent;
+import org.bukkit.inventory.ItemStack;
+import uk.antiperson.stackmob.entity.StackEntity;
+
+import java.util.Map;
+
+/**
+ * Event called when a stack drops items.
+ */
+public class StackDropItemEvent extends StackEvent {
+
+ private static final HandlerList handlers = new HandlerList();
+ private Map drops;
+ private EntityDeathEvent originalEvent;
+
+ public StackDropItemEvent(
+ StackEntity stackEntity,
+ Map drops,
+ EntityDeathEvent originalEvent
+ ) {
+ super(stackEntity);
+ this.drops = drops;
+ this.originalEvent = originalEvent;
+ }
+
+ public HandlerList getHandlers() {
+ return handlers;
+ }
+
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+
+ /**
+ * Gets the map of drops that will be dropped when the stack is killed.
+ * This map should be modified to change the drops.
+ * @return the map of drops that will be dropped when the stack is killed.
+ */
+ public Map getDrops() {
+ return drops;
+ }
+
+ /**
+ * Returns the original event that triggered this drop item event.
+ * @return the original event that triggered this drop item event.
+ */
+ public EntityDeathEvent getOriginalEvent() {
+ return originalEvent;
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/events/StackSpawnEvent.java b/src/main/java/uk/antiperson/stackmob/events/StackSpawnEvent.java
new file mode 100644
index 00000000..00bca12d
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/events/StackSpawnEvent.java
@@ -0,0 +1,42 @@
+package uk.antiperson.stackmob.events;
+
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+
+/**
+ * Event called when a newly spawned entity is about to be stacked.
+ */
+public class StackSpawnEvent extends Event implements Cancellable {
+
+ private static final HandlerList handlers = new HandlerList();
+ private boolean cancelled;
+ private final LivingEntity livingEntity;
+
+ public StackSpawnEvent(LivingEntity livingEntity) {
+ this.livingEntity = livingEntity;
+ }
+
+ public LivingEntity getLivingEntity() {
+ return livingEntity;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancel) {
+ this.cancelled = cancel;
+ }
+
+ public HandlerList getHandlers() {
+ return handlers;
+ }
+
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/hook/HookManager.java b/src/main/java/uk/antiperson/stackmob/hook/HookManager.java
index d65b9d7d..791c3e46 100644
--- a/src/main/java/uk/antiperson/stackmob/hook/HookManager.java
+++ b/src/main/java/uk/antiperson/stackmob/hook/HookManager.java
@@ -2,6 +2,7 @@
import org.bukkit.Location;
import org.bukkit.entity.LivingEntity;
+import org.bukkit.event.Listener;
import uk.antiperson.stackmob.StackMob;
import uk.antiperson.stackmob.entity.StackEntity;
import uk.antiperson.stackmob.hook.hooks.*;
@@ -13,11 +14,11 @@
public class HookManager {
- private final HashSet hooks = new HashSet<>();
+ private final HashSet hooks;
private final StackMob sm;
- private ProtocolLibHook protocolLibHook;
public HookManager(StackMob sm) {
this.sm = sm;
+ hooks = new HashSet<>();
}
/**
@@ -42,11 +43,13 @@ public void registerOnLoad() throws InstantiationException, IllegalAccessExcepti
*/
public void registerHooks() throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
registerHook(WorldGuardHook.class);
- registerHook(MythicMobsHook.class);
+ registerHook(MythicMobsStackHook.class);
+ registerHook(MythicMobsNoStackHook.class);
registerHook(McmmoHook.class);
registerHook(CitizensHook.class);
registerHook(JobsHook.class);
- registerHook(ProtocolLibHook.class);
+ registerHook(ClearlaggHook.class);
+ registerHook(MyPetHook.class);
}
/**
@@ -62,10 +65,13 @@ private void registerHook(Class extends Hook> hookClass) throws IllegalAccessE
if (!sm.getServer().getPluginManager().isPluginEnabled(hookMetadata.name())) {
return;
}
- if (!sm.getMainConfig().isHookEnabled(hookMetadata.config())) {
+ if (!sm.getMainConfig().getConfig().isHookEnabled(hookMetadata.config())) {
return;
}
Hook hook = createInstance(hookClass);
+ if (hook instanceof Listener) {
+ sm.getServer().getPluginManager().registerEvents((Listener) hook, sm);
+ }
hook.onEnable();
hooks.add(hook);
}
@@ -80,7 +86,7 @@ private void registerHook(Class extends Hook> hookClass) throws IllegalAccessE
* @throws InstantiationException if the class is abstract
*/
private Hook createInstance(Class extends Hook> hookClass) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
- for (Constructor constructor : hookClass.getDeclaredConstructors()) {
+ for (Constructor> constructor : hookClass.getDeclaredConstructors()) {
for (Parameter parameter : constructor.getParameters()) {
if (parameter.getType().isAssignableFrom(StackMob.class)) {
return hookClass.getDeclaredConstructor(StackMob.class).newInstance(sm);
@@ -103,8 +109,8 @@ public boolean checkHooks(StackEntity first, StackEntity nearby) {
if (!ph.canStack(first.getEntity()) || !ph.canStack(nearby.getEntity())) {
return true;
}
- } else if (hook instanceof SegregatedMobHook) {
- SegregatedMobHook smh = (SegregatedMobHook) hook;
+ } else if (hook instanceof PreventStackHook) {
+ PreventStackHook smh = (PreventStackHook) hook;
if (smh.isCustomMob(first.getEntity()) || smh.isCustomMob(nearby.getEntity())) {
return true;
}
@@ -120,6 +126,18 @@ public boolean checkHooks(StackEntity first, StackEntity nearby) {
return false;
}
+ public boolean spawnCheck(LivingEntity stackEntity) {
+ for (Hook hook : hooks) {
+ if (hook instanceof PreventStackHook) {
+ PreventStackHook smh = (PreventStackHook) hook;
+ if (smh.isCustomMob(stackEntity)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
public void onSpawn(StackEntity entity) {
for (Hook hook : hooks) {
if (hook instanceof SpawnHook) {
@@ -154,16 +172,4 @@ public StackableMobHook getApplicableHook(StackEntity entity) {
return null;
}
- public ProtocolLibHook getProtocolLibHook() {
- if (protocolLibHook != null) {
- return protocolLibHook;
- }
- for (Hook hook : hooks) {
- if (hook instanceof ProtocolLibHook) {
- protocolLibHook = (ProtocolLibHook) hook;
- }
- }
- return protocolLibHook;
- }
-
}
diff --git a/src/main/java/uk/antiperson/stackmob/hook/PreventStackHook.java b/src/main/java/uk/antiperson/stackmob/hook/PreventStackHook.java
new file mode 100644
index 00000000..6ef3679e
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/hook/PreventStackHook.java
@@ -0,0 +1,4 @@
+package uk.antiperson.stackmob.hook;
+
+public interface PreventStackHook extends CustomMobHook {
+}
diff --git a/src/main/java/uk/antiperson/stackmob/hook/SegregatedMobHook.java b/src/main/java/uk/antiperson/stackmob/hook/SegregatedMobHook.java
deleted file mode 100644
index 0241b9c8..00000000
--- a/src/main/java/uk/antiperson/stackmob/hook/SegregatedMobHook.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package uk.antiperson.stackmob.hook;
-
-public interface SegregatedMobHook extends CustomMobHook {
-}
diff --git a/src/main/java/uk/antiperson/stackmob/hook/hooks/CitizensHook.java b/src/main/java/uk/antiperson/stackmob/hook/hooks/CitizensHook.java
index 1bab1506..297dee4b 100644
--- a/src/main/java/uk/antiperson/stackmob/hook/hooks/CitizensHook.java
+++ b/src/main/java/uk/antiperson/stackmob/hook/hooks/CitizensHook.java
@@ -4,10 +4,10 @@
import uk.antiperson.stackmob.StackMob;
import uk.antiperson.stackmob.hook.Hook;
import uk.antiperson.stackmob.hook.HookMetadata;
-import uk.antiperson.stackmob.hook.SegregatedMobHook;
+import uk.antiperson.stackmob.hook.PreventStackHook;
@HookMetadata(name = "Citizens", config = "citizens")
-public class CitizensHook extends Hook implements SegregatedMobHook {
+public class CitizensHook extends Hook implements PreventStackHook {
public CitizensHook(StackMob sm) {
super(sm);
diff --git a/src/main/java/uk/antiperson/stackmob/hook/hooks/ClearlaggHook.java b/src/main/java/uk/antiperson/stackmob/hook/hooks/ClearlaggHook.java
new file mode 100644
index 00000000..77ba9016
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/hook/hooks/ClearlaggHook.java
@@ -0,0 +1,24 @@
+package uk.antiperson.stackmob.hook.hooks;
+
+import me.minebuilders.clearlag.events.EntityRemoveEvent;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.entity.StackEntity;
+import uk.antiperson.stackmob.hook.Hook;
+import uk.antiperson.stackmob.hook.HookMetadata;
+
+@HookMetadata(name = "ClearLag", config = "clearlagg")
+public class ClearlaggHook extends Hook implements Listener {
+
+ public ClearlaggHook(StackMob sm) {
+ super(sm);
+ }
+
+ @EventHandler
+ public void onEntityRemove(EntityRemoveEvent event) {
+ for (StackEntity stackEntity : sm.getEntityManager().getStackEntities()) {
+ event.addEntity(stackEntity.getEntity());
+ }
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/hook/hooks/JobsHook.java b/src/main/java/uk/antiperson/stackmob/hook/hooks/JobsHook.java
index c82855cd..414826f9 100644
--- a/src/main/java/uk/antiperson/stackmob/hook/hooks/JobsHook.java
+++ b/src/main/java/uk/antiperson/stackmob/hook/hooks/JobsHook.java
@@ -1,16 +1,24 @@
package uk.antiperson.stackmob.hook.hooks;
import com.gamingmesh.jobs.Jobs;
+import com.gamingmesh.jobs.api.JobsPaymentEvent;
+import com.gamingmesh.jobs.api.JobsPrePaymentEvent;
+import com.gamingmesh.jobs.container.CurrencyType;
import org.bukkit.entity.LivingEntity;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
import org.bukkit.metadata.FixedMetadataValue;
import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.entity.StackEntity;
import uk.antiperson.stackmob.hook.Hook;
import uk.antiperson.stackmob.hook.HookMetadata;
import uk.antiperson.stackmob.hook.SpawnHook;
+import uk.antiperson.stackmob.utils.Utilities;
-@HookMetadata(name = "Jobs", config = "jobs")
-public class JobsHook extends Hook implements SpawnHook {
+@HookMetadata(name = "Jobs", config = "jobs.enabled")
+public class JobsHook extends Hook implements SpawnHook, Listener {
+ private StackEntity stackEntity;
public JobsHook(StackMob sm) {
super(sm);
}
@@ -19,4 +27,33 @@ public JobsHook(StackMob sm) {
public void onSpawn(LivingEntity spawned) {
spawned.setMetadata(Jobs.getPlayerManager().getMobSpawnerMetadata(), new FixedMetadataValue(getPlugin(), true));
}
+
+ @EventHandler
+ public void onJobsPrePayment(JobsPrePaymentEvent event) {
+ if (event.getLivingEntity() == null || Utilities.IS_FOLIA) { // TODO: Make this work with Folia.
+ return;
+ }
+ stackEntity = sm.getEntityManager().getStackEntity(event.getLivingEntity());
+ }
+
+ @EventHandler
+ public void onJobsPayment(JobsPaymentEvent event) {
+ if (stackEntity == null || sm.getMainConfig().getConfig().getJobHookMode() == JobHookMode.IGNORE) {
+ return;
+ }
+ for (CurrencyType currencyType : CurrencyType.values()) {
+ if (sm.getMainConfig().getConfig().getJobHookMode() == JobHookMode.PREVENT) {
+ event.set(currencyType, 0);
+ continue;
+ }
+ event.set(currencyType, event.get(currencyType) * stackEntity.getSize());
+ }
+ stackEntity = null;
+ }
+
+ public enum JobHookMode {
+ PREVENT,
+ IGNORE,
+ MULTIPLY
+ }
}
diff --git a/src/main/java/uk/antiperson/stackmob/hook/hooks/MyPetHook.java b/src/main/java/uk/antiperson/stackmob/hook/hooks/MyPetHook.java
new file mode 100644
index 00000000..24f84ddc
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/hook/hooks/MyPetHook.java
@@ -0,0 +1,34 @@
+package uk.antiperson.stackmob.hook.hooks;
+
+import de.Keyle.MyPet.MyPetApi;
+import de.Keyle.MyPet.api.entity.MyPet;
+import de.Keyle.MyPet.api.entity.MyPetBukkitEntity;
+import org.bukkit.entity.LivingEntity;
+import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.hook.Hook;
+import uk.antiperson.stackmob.hook.HookMetadata;
+import uk.antiperson.stackmob.hook.PreventStackHook;
+
+import java.util.Optional;
+
+@HookMetadata(name = "MyPet", config = "mypet")
+public class MyPetHook extends Hook implements PreventStackHook {
+
+ public MyPetHook(StackMob sm) {
+ super(sm);
+ }
+
+ @Override
+ public boolean isCustomMob(LivingEntity entity) {
+ for (MyPet myPet : MyPetApi.getMyPetManager().getAllActiveMyPets()) {
+ Optional optional = myPet.getEntity();
+ if (!optional.isPresent()) {
+ continue;
+ }
+ if (optional.get().getUniqueId().equals(entity.getUniqueId())) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/hook/hooks/MythicMobsNoStackHook.java b/src/main/java/uk/antiperson/stackmob/hook/hooks/MythicMobsNoStackHook.java
new file mode 100644
index 00000000..d21a443d
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/hook/hooks/MythicMobsNoStackHook.java
@@ -0,0 +1,27 @@
+package uk.antiperson.stackmob.hook.hooks;
+
+import io.lumine.mythic.bukkit.MythicBukkit;
+import org.bukkit.entity.LivingEntity;
+import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.hook.Hook;
+import uk.antiperson.stackmob.hook.HookMetadata;
+import uk.antiperson.stackmob.hook.PreventStackHook;
+
+@HookMetadata(name = "MythicMobs", config = "mythicmobs.prevent-stack")
+public class MythicMobsNoStackHook extends Hook implements PreventStackHook {
+
+ private MythicBukkit mythicMobs;
+ public MythicMobsNoStackHook(StackMob sm) {
+ super(sm);
+ }
+
+ @Override
+ public boolean isCustomMob(LivingEntity entity) {
+ return mythicMobs.getMobManager().isActiveMob(entity.getUniqueId());
+ }
+
+ @Override
+ public void onEnable() {
+ mythicMobs = (MythicBukkit) getPlugin();
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/hook/hooks/MythicMobsHook.java b/src/main/java/uk/antiperson/stackmob/hook/hooks/MythicMobsStackHook.java
similarity index 79%
rename from src/main/java/uk/antiperson/stackmob/hook/hooks/MythicMobsHook.java
rename to src/main/java/uk/antiperson/stackmob/hook/hooks/MythicMobsStackHook.java
index 8400d715..1a6ee40d 100644
--- a/src/main/java/uk/antiperson/stackmob/hook/hooks/MythicMobsHook.java
+++ b/src/main/java/uk/antiperson/stackmob/hook/hooks/MythicMobsStackHook.java
@@ -1,7 +1,7 @@
package uk.antiperson.stackmob.hook.hooks;
-import io.lumine.xikage.mythicmobs.MythicMobs;
-import io.lumine.xikage.mythicmobs.mobs.ActiveMob;
+import io.lumine.mythic.bukkit.MythicBukkit;
+import io.lumine.mythic.core.mobs.ActiveMob;
import org.bukkit.Location;
import org.bukkit.entity.LivingEntity;
import uk.antiperson.stackmob.StackMob;
@@ -10,11 +10,11 @@
import uk.antiperson.stackmob.hook.HookMetadata;
import uk.antiperson.stackmob.hook.StackableMobHook;
-@HookMetadata(name = "MythicMobs", config = "mythicmobs.enabled")
-public class MythicMobsHook extends Hook implements StackableMobHook {
+@HookMetadata(name = "MythicMobs", config = "mythicmobs.stack")
+public class MythicMobsStackHook extends Hook implements StackableMobHook {
- private MythicMobs mythicMobs;
- public MythicMobsHook(StackMob sm) {
+ private MythicBukkit mythicMobs;
+ public MythicMobsStackHook(StackMob sm) {
super(sm);
}
@@ -25,7 +25,7 @@ public boolean isMatching(LivingEntity first, LivingEntity nearby) {
if(!(activeMobO.getType().equals(activeMobN.getType()))){
return false;
}
- ConfigList list = sm.getMainConfig().getList(first.getType(), "hooks.mythicmobs.blacklist");
+ ConfigList list = sm.getMainConfig().getConfigFile().getList("hooks.mythicmobs.stack-blacklist");
return !list.contains(activeMobN.getType().getInternalName());
}
@@ -55,6 +55,6 @@ public boolean isCustomMob(LivingEntity entity) {
@Override
public void onEnable() {
- mythicMobs = (MythicMobs) getPlugin();
+ mythicMobs = (MythicBukkit) getPlugin();
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/hook/hooks/ProtocolLibHook.java b/src/main/java/uk/antiperson/stackmob/hook/hooks/ProtocolLibHook.java
deleted file mode 100644
index be956802..00000000
--- a/src/main/java/uk/antiperson/stackmob/hook/hooks/ProtocolLibHook.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package uk.antiperson.stackmob.hook.hooks;
-
-import com.comphenix.protocol.PacketType;
-import com.comphenix.protocol.ProtocolLibrary;
-import com.comphenix.protocol.ProtocolManager;
-import com.comphenix.protocol.events.PacketContainer;
-import com.comphenix.protocol.wrappers.WrappedDataWatcher;
-import org.bukkit.entity.Entity;
-import org.bukkit.entity.Player;
-import uk.antiperson.stackmob.StackMob;
-import uk.antiperson.stackmob.hook.Hook;
-import uk.antiperson.stackmob.hook.HookMetadata;
-
-import java.lang.reflect.InvocationTargetException;
-
-@HookMetadata(name = "ProtocolLib", config = "protocollib")
-public class ProtocolLibHook extends Hook {
-
- private ProtocolManager protocolManager;
- public ProtocolLibHook(StackMob sm) {
- super(sm);
- }
-
- @Override
- public void onEnable() {
- protocolManager = ProtocolLibrary.getProtocolManager();
- }
-
- public void sendPacket(Player player, Entity entity, boolean visible) {
- PacketContainer packetContainer = new PacketContainer(PacketType.Play.Server.ENTITY_METADATA);
- WrappedDataWatcher watcher = new WrappedDataWatcher(entity);
- watcher.setObject(new WrappedDataWatcher.WrappedDataWatcherObject(3, WrappedDataWatcher.Registry.get(Boolean.class)), visible);
- packetContainer.getEntityModifier(entity.getWorld()).write(0, entity);
- packetContainer.getWatchableCollectionModifier().write(0, watcher.getWatchableObjects());
- try {
- protocolManager.sendServerPacket(player, packetContainer);
- } catch (InvocationTargetException e) {
- e.printStackTrace();
- }
- }
-}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/BeeListener.java b/src/main/java/uk/antiperson/stackmob/listeners/BeeListener.java
new file mode 100644
index 00000000..37157f2e
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/listeners/BeeListener.java
@@ -0,0 +1,32 @@
+package uk.antiperson.stackmob.listeners;
+
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityEnterBlockEvent;
+import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.entity.StackEntity;
+
+@ListenerMetadata(config = "events.divide.enter-block")
+public class BeeListener implements Listener {
+
+ private final StackMob sm;
+
+ public BeeListener(StackMob sm) {
+ this.sm = sm;
+ }
+
+ @EventHandler (ignoreCancelled = true)
+ public void onEntityEnterBlockEvent(EntityEnterBlockEvent event) {
+ LivingEntity bee = (LivingEntity) event.getEntity();
+ if (!sm.getEntityManager().isStackedEntity(bee)) {
+ return;
+ }
+ final StackEntity oldBee = sm.getEntityManager().getStackEntity(bee);
+ if (oldBee == null || oldBee.isSingle()) {
+ return;
+ }
+ oldBee.slice();
+ }
+
+}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/BreedInteractListener.java b/src/main/java/uk/antiperson/stackmob/listeners/BreedInteractListener.java
index 37a47c54..f3d15eaf 100644
--- a/src/main/java/uk/antiperson/stackmob/listeners/BreedInteractListener.java
+++ b/src/main/java/uk/antiperson/stackmob/listeners/BreedInteractListener.java
@@ -7,8 +7,10 @@
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.config.EntityConfig;
+import uk.antiperson.stackmob.entity.EntityFood;
import uk.antiperson.stackmob.entity.StackEntity;
-import uk.antiperson.stackmob.utils.EntityUtils;
+import uk.antiperson.stackmob.utils.Utilities;
@ListenerMetadata(config = "events.breed.enabled")
public class BreedInteractListener implements Listener {
@@ -29,46 +31,39 @@ public void onBreedInteract(PlayerInteractEntityEvent event) {
if (!(event.getRightClicked() instanceof Animals)) {
return;
}
- if (!((Animals) event.getRightClicked()).canBreed()) {
+ Animals animals = (Animals) event.getRightClicked();
+ if (!animals.canBreed()) {
return;
}
ItemStack foodItem = event.getPlayer().getInventory().getItemInMainHand();
- if (!EntityUtils.isCorrectFood(event.getRightClicked(), foodItem.getType())) {
+ if (!EntityFood.isCorrectFood(event.getRightClicked(), foodItem.getType())) {
return;
}
- Animals animals = (Animals) event.getRightClicked();
StackEntity stackEntity = sm.getEntityManager().getStackEntity(animals);
- if (stackEntity.isSingle()) {
+ if (stackEntity == null || stackEntity.isSingle()) {
+ return;
+ }
+ EntityConfig.ListenerMode breed = stackEntity.getEntityConfig().getListenerMode(EntityConfig.EventType.BREED);
+ if (breed == EntityConfig.ListenerMode.SPLIT) {
+ stackEntity.slice();
return;
}
- switch (sm.getMainConfig().getListenerMode(animals.getType(), "breed")) {
- case SPLIT:
- stackEntity.slice();
- break;
- case MULTIPLY:
- int itemAmount = event.getPlayer().getInventory().getItemInMainHand().getAmount();
- EntityUtils.removeHandItem(event.getPlayer(), stackEntity.getSize());
- stackEntity.splitIfNotEnough(itemAmount);
- if (itemAmount == 1) {
- return;
- }
- double kAmount = stackEntity.getSize() / 2D;
- int kidAmount = (int) Math.floor(kAmount);
- if (kAmount > kidAmount) {
- stackEntity.duplicate();
- stackEntity.incrementSize(-1);
- }
- stackEntity.getDrops().dropExperience(event.getRightClicked().getLocation(),1,7, kidAmount);
- // Spawn the kid
- StackEntity kid = stackEntity.duplicate();
- kid.setSize(kidAmount);
- ((Animals) kid.getEntity()).setBaby();
- // Update the adult
- animals.setBreed(false);
- animals.setBreedCause(event.getPlayer().getUniqueId());
- break;
+ int itemAmount = event.getPlayer().getInventory().getItemInMainHand().getAmount();
+ stackEntity.splitIfNotEnough(itemAmount);
+ if (itemAmount == 1) {
+ Utilities.removeHandItem(event.getPlayer(), 1);
+ return;
+ }
+ int kidAmount = stackEntity.getEntityConfig().getEventMultiplyLimit(EntityConfig.EventType.BREED, stackEntity.getSize() / 2);
+ int parentAmount = kidAmount * 2;
+ if (stackEntity.getSize() > parentAmount) {
+ stackEntity.slice(parentAmount);
}
+ Utilities.removeHandItem(event.getPlayer(), parentAmount);
+ stackEntity.getDrops().dropExperience(event.getRightClicked().getLocation(),1,7, kidAmount);
+ stackEntity.spawnChild(kidAmount);
+ // Update the adult
+ animals.setBreed(false);
+ animals.setBreedCause(event.getPlayer().getUniqueId());
}
-
-
}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/BucketListener.java b/src/main/java/uk/antiperson/stackmob/listeners/BucketListener.java
new file mode 100644
index 00000000..ecb65c1b
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/listeners/BucketListener.java
@@ -0,0 +1,35 @@
+package uk.antiperson.stackmob.listeners;
+
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerBucketEntityEvent;
+import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.entity.StackEntity;
+
+@ListenerMetadata(config = "events.divide.bucket-fill")
+public class BucketListener implements Listener {
+
+ private final StackMob sm;
+
+ public BucketListener(StackMob sm) {
+ this.sm = sm;
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onBucketFill(PlayerBucketEntityEvent event) {
+ final StackEntity stackEntity = sm.getEntityManager().getStackEntity((LivingEntity) event.getEntity());
+ if (stackEntity == null) {
+ return;
+ }
+ if (stackEntity.isSingle()) {
+ return;
+ }
+ stackEntity.slice();
+ stackEntity.remove();
+ event.getEntityBucket().setItemMeta(null);
+ }
+
+
+}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/ChunkListener.java b/src/main/java/uk/antiperson/stackmob/listeners/ChunkListener.java
new file mode 100644
index 00000000..f636d098
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/listeners/ChunkListener.java
@@ -0,0 +1,25 @@
+package uk.antiperson.stackmob.listeners;
+
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.world.ChunkLoadEvent;
+import org.bukkit.event.world.ChunkUnloadEvent;
+import uk.antiperson.stackmob.StackMob;
+
+public class ChunkListener implements Listener {
+
+ private StackMob sm;
+ public ChunkListener(StackMob sm) {
+ this.sm = sm;
+ }
+
+ @EventHandler
+ public void onChunkLoad(ChunkLoadEvent event) {
+ sm.getEntityManager().registerStackedEntities(event.getChunk());
+ }
+
+ @EventHandler
+ public void onChunkUnload(ChunkUnloadEvent event) {
+ sm.getEntityManager().unregisterStackedEntities(event.getChunk());
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/DeathListener.java b/src/main/java/uk/antiperson/stackmob/listeners/DeathListener.java
index 41cbeb1a..5d1e9ea5 100644
--- a/src/main/java/uk/antiperson/stackmob/listeners/DeathListener.java
+++ b/src/main/java/uk/antiperson/stackmob/listeners/DeathListener.java
@@ -1,6 +1,8 @@
package uk.antiperson.stackmob.listeners;
import org.bukkit.Statistic;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.ExperienceOrb;
import org.bukkit.entity.Slime;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@@ -8,13 +10,17 @@
import org.bukkit.inventory.ItemStack;
import org.bukkit.metadata.FixedMetadataValue;
import uk.antiperson.stackmob.StackMob;
-import uk.antiperson.stackmob.entity.death.DeathType;
import uk.antiperson.stackmob.entity.Drops;
import uk.antiperson.stackmob.entity.StackEntity;
import uk.antiperson.stackmob.entity.death.DeathMethod;
+import uk.antiperson.stackmob.entity.death.DeathType;
import uk.antiperson.stackmob.events.EventHelper;
+import uk.antiperson.stackmob.events.StackDeathEvent;
+import uk.antiperson.stackmob.events.StackDropItemEvent;
+import uk.antiperson.stackmob.utils.Utilities;
import java.lang.reflect.InvocationTargetException;
+import java.util.List;
import java.util.Map;
public class DeathListener implements Listener {
@@ -30,40 +36,65 @@ public void onStackDeath(EntityDeathEvent event) {
return;
}
StackEntity stackEntity = sm.getEntityManager().getStackEntity(event.getEntity());
- if (stackEntity.isSingle()) {
- return;
- }
- event.getEntity().setCustomName("");
- event.getEntity().setCustomNameVisible(false);
DeathMethod deathMethod = calculateDeath(stackEntity);
int deathStep = Math.min(stackEntity.getSize(), deathMethod.calculateStep());
- EventHelper.callStackDeathEvent(stackEntity, deathStep);
- if (stackEntity.getSize() > deathStep) {
- StackEntity spawned = stackEntity.duplicate();
- spawned.setSize(stackEntity.getSize() - deathStep);
- deathMethod.onSpawn(spawned);
+ StackDeathEvent stackDeathEvent = EventHelper.callStackDeathEvent(stackEntity, deathStep);
+ deathStep = stackDeathEvent.getDeathStep();
+ int toMultiply = deathStep - 1;
+ if (sm.getMainConfig().getConfigFile().getBoolean("traits.leashed")) {
+ if (event.getEntity().isLeashed() && (stackEntity.getSize() - deathStep) != 0) {
+ event.getEntity().setMetadata(Utilities.NO_LEASH_METADATA, new FixedMetadataValue(sm, true));
+ }
+ }
+ if (deathStep < stackEntity.getSize()) {
+ if (stackEntity.getEntityConfig().isSkipDeathAnimation()) {
+ toMultiply = deathStep;
+ event.setCancelled(true);
+ stackEntity.incrementSize(-deathStep);
+ deathMethod.onSpawn(stackEntity);
+ } else {
+ int finalDeathStep = deathStep;
+ sm.getScheduler().runTask(event.getEntity(), () -> {
+ StackEntity spawned = stackEntity.duplicate();
+ spawned.setSize(stackEntity.getSize() - finalDeathStep);
+ deathMethod.onSpawn(spawned);
+ });
+ }
}
- if (deathStep <= 1) {
+ if (!stackEntity.getEntityConfig().isSkipDeathAnimation()) {
+ stackEntity.removeStackData();
+ }
+ if (toMultiply == 0) {
return;
}
- int toMultiply = deathStep - 1;
int experience = stackEntity.getDrops().calculateDeathExperience(toMultiply, event.getDroppedExp());
- Map drops = stackEntity.getDrops().calculateDrops(toMultiply, event.getDrops());
- Drops.dropItems(event.getEntity().getLocation(), drops);
+ // Workaround for craftbukkit bug?/change
+ // Enchantment effects are now applied after the death event is fired....
+ // Should probably investigate more...? How are the drops in the event correct.
+ if (Utilities.isVersionAtLeast(Utilities.MinecraftVersion.V1_21) && stackEntity.getEntityConfig().isDropLootTables()) {
+ int finalToMultiply = toMultiply;
+ Runnable runnable = () -> doDrops(stackEntity, finalToMultiply, event.getDrops(), event);
+ sm.getScheduler().runTaskLater(stackEntity.getEntity(), runnable, 1);
+ } else {
+ doDrops(stackEntity, toMultiply, event.getDrops(), event);
+ }
event.setDroppedExp(experience);
- if (sm.getMainConfig().isPlayerStatMulti(event.getEntityType())) {
+ if (Utilities.isPaper() && event.isCancelled()) {
+ ExperienceOrb orb = (ExperienceOrb) event.getEntity().getWorld().spawnEntity(event.getEntity().getLocation(), EntityType.EXPERIENCE_ORB);
+ orb.setExperience(experience);
+ }
+ if (stackEntity.getEntityConfig().isPlayerStatMulti()) {
if (event.getEntity().getKiller() != null) {
event.getEntity().getKiller().incrementStatistic(Statistic.KILL_ENTITY, event.getEntityType(), toMultiply);
}
}
- if (event.getEntity() instanceof Slime && sm.getMainConfig().isSlimeMultiEnabled(event.getEntityType())) {
+ if (event.getEntity() instanceof Slime && stackEntity.getEntityConfig().isSlimeMultiEnabled()) {
event.getEntity().setMetadata("deathcount", new FixedMetadataValue(sm, toMultiply));
}
-
}
- public DeathMethod calculateDeath(StackEntity entity){
- DeathType deathType = sm.getMainConfig().getDeathType(entity.getEntity());
+ public DeathMethod calculateDeath(StackEntity entity) {
+ DeathType deathType = entity.getEntityConfig().getDeathType(entity.getEntity());
try {
return deathType.getStepClass().getDeclaredConstructor(StackMob.class, StackEntity.class).newInstance(sm, entity);
} catch (InstantiationException | InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
@@ -71,7 +102,15 @@ public DeathMethod calculateDeath(StackEntity entity){
}
}
-
-
+ private void doDrops(
+ StackEntity stackEntity,
+ int toMultiply,
+ List drops,
+ EntityDeathEvent event
+ ) {
+ Map map = stackEntity.getDrops().calculateDrops(toMultiply, drops);
+ EventHelper.callStackDropItemEvent(stackEntity, map, event);
+ Drops.dropItems(stackEntity.getEntity().getLocation(), map);
+ }
}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/DropListener.java b/src/main/java/uk/antiperson/stackmob/listeners/DropListener.java
index 49f79386..9fd7f79a 100644
--- a/src/main/java/uk/antiperson/stackmob/listeners/DropListener.java
+++ b/src/main/java/uk/antiperson/stackmob/listeners/DropListener.java
@@ -1,13 +1,16 @@
package uk.antiperson.stackmob.listeners;
import org.bukkit.Material;
+import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
+import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDropItemEvent;
import uk.antiperson.stackmob.StackMob;
import uk.antiperson.stackmob.entity.Drops;
import uk.antiperson.stackmob.entity.StackEntity;
+import uk.antiperson.stackmob.utils.Utilities;
import java.util.concurrent.ThreadLocalRandom;
@@ -22,7 +25,9 @@ public DropListener(StackMob sm) {
@EventHandler
public void onDropListener(EntityDropItemEvent event) {
- if (event.getItemDrop().getItemStack().getType() != Material.EGG && event.getItemDrop().getItemStack().getType() != Material.SCUTE) {
+ if (!(event.getEntity() instanceof Villager) &&
+ event.getItemDrop().getItemStack().getType() != Material.EGG &&
+ event.getItemDrop().getItemStack().getType() != Utilities.getScuteMaterial()) {
return;
}
if (!sm.getEntityManager().isStackedEntity((LivingEntity) event.getEntity())) {
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/DyeListener.java b/src/main/java/uk/antiperson/stackmob/listeners/DyeListener.java
index 0993896f..34330d24 100644
--- a/src/main/java/uk/antiperson/stackmob/listeners/DyeListener.java
+++ b/src/main/java/uk/antiperson/stackmob/listeners/DyeListener.java
@@ -8,8 +8,9 @@
import org.bukkit.inventory.ItemStack;
import org.bukkit.material.Colorable;
import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.config.EntityConfig;
import uk.antiperson.stackmob.entity.StackEntity;
-import uk.antiperson.stackmob.utils.EntityUtils;
+import uk.antiperson.stackmob.utils.Utilities;
@ListenerMetadata(config = "events.dye.enabled")
public class DyeListener implements Listener {
@@ -29,24 +30,24 @@ public void onDyeListener(PlayerInteractEntityEvent event) {
return;
}
ItemStack handItem = event.getPlayer().getInventory().getItemInMainHand();
- if (!EntityUtils.isDye(handItem)) {
+ if (!Utilities.isDye(handItem)) {
return;
}
Sheep sheep = (Sheep) event.getRightClicked();
StackEntity stackEntity = sm.getEntityManager().getStackEntity(sheep);
- if (stackEntity.isSingle()) {
+ if (stackEntity == null || stackEntity.isSingle()) {
return;
}
- switch (sm.getMainConfig().getListenerMode(sheep.getType(), "dye")) {
- case SPLIT:
- StackEntity slice = stackEntity.slice();
- ((Colorable) slice.getEntity()).setColor(sheep.getColor());
- break;
- case MULTIPLY:
- stackEntity.splitIfNotEnough(event.getPlayer().getInventory().getItemInMainHand().getAmount());
- EntityUtils.removeHandItem(event.getPlayer(), stackEntity.getSize());
- sheep.setColor(DyeColor.valueOf(handItem.getType().toString().replace("_DYE", "")));
- break;
+ EntityConfig.ListenerMode mode = stackEntity.getEntityConfig().getListenerMode(EntityConfig.EventType.DYE);
+ if (mode == EntityConfig.ListenerMode.SPLIT) {
+ ((Colorable) stackEntity.slice().getEntity()).setColor(sheep.getColor());
+ return;
}
+ int items = event.getPlayer().getInventory().getItemInMainHand().getAmount();
+ int limit = stackEntity.getEntityConfig().getEventMultiplyLimit(EntityConfig.EventType.DYE, stackEntity.getSize());
+ int toSlice = Math.min(limit, items);
+ stackEntity.splitIfNotEnough(toSlice);
+ Utilities.removeHandItem(event.getPlayer(), toSlice);
+ sheep.setColor(DyeColor.valueOf(handItem.getType().toString().replace("_DYE", "")));
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/EquipListener.java b/src/main/java/uk/antiperson/stackmob/listeners/EquipListener.java
new file mode 100644
index 00000000..bed2b40a
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/listeners/EquipListener.java
@@ -0,0 +1,47 @@
+package uk.antiperson.stackmob.listeners;
+
+import org.bukkit.Material;
+import org.bukkit.entity.Mob;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityPickupItemEvent;
+import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.entity.StackEntity;
+
+import java.util.Arrays;
+import java.util.List;
+
+@ListenerMetadata(config = "events.equip.enabled")
+public class EquipListener implements Listener {
+
+ private final StackMob sm;
+
+ public EquipListener(StackMob sm) {
+ this.sm = sm;
+ }
+
+ @EventHandler
+ public void onEntityEquip(EntityPickupItemEvent event) {
+ if (!(event.getEntity() instanceof Mob)) {
+ return;
+ }
+ if (!isArmour(event.getItem().getItemStack().getType())) {
+ return;
+ }
+ if (!sm.getEntityManager().isStackedEntity(event.getEntity())) {
+ return;
+ }
+ StackEntity stackEntity = sm.getEntityManager().getStackEntity(event.getEntity());
+ stackEntity.addEquipItem(event.getItem().getItemStack());
+ }
+
+ private boolean isArmour(Material material) {
+ List endings = Arrays.asList("HELMET", "CHESTPLATE", "LEGGINGS", "BOOTS", "AXE", "SHOVEL", "HOE", "SWORD", "SHIELD");
+ for (String ending : endings) {
+ if (material.toString().endsWith(ending)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/ExplosionListener.java b/src/main/java/uk/antiperson/stackmob/listeners/ExplosionListener.java
index d5baf634..bdbb9528 100644
--- a/src/main/java/uk/antiperson/stackmob/listeners/ExplosionListener.java
+++ b/src/main/java/uk/antiperson/stackmob/listeners/ExplosionListener.java
@@ -5,11 +5,12 @@
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityExplodeEvent;
import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.config.EntityConfig;
import uk.antiperson.stackmob.entity.StackEntity;
import java.util.concurrent.ThreadLocalRandom;
-@ListenerMetadata(config = "events.multiply.explosion")
+@ListenerMetadata(config = "events.explosion.enabled")
public class ExplosionListener implements Listener {
private final StackMob sm;
@@ -24,10 +25,18 @@ public void onExplosion(EntityExplodeEvent event) {
return;
}
StackEntity stackEntity = sm.getEntityManager().getStackEntity((LivingEntity) event.getEntity());
- if (stackEntity.isSingle()) {
+ if (stackEntity == null || stackEntity.isSingle()) {
return;
}
- double multiplier = ThreadLocalRandom.current().nextDouble(0.4, 0.6);
- event.setYield(event.getYield() + Math.round(event.getYield() * stackEntity.getSize() * multiplier));
+ switch (stackEntity.getEntityConfig().getListenerMode(EntityConfig.EventType.EXPLOSION)) {
+ case SPLIT:
+ stackEntity.slice();
+ break;
+ case MULTIPLY:
+ double multiplier = ThreadLocalRandom.current().nextDouble(0.4, 0.6);
+ int toMultiply = stackEntity.getEntityConfig().getEventMultiplyLimit(EntityConfig.EventType.EXPLOSION, stackEntity.getSize());
+ event.setYield(event.getYield() + Math.round(event.getYield() * toMultiply * multiplier));
+ break;
+ }
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/KnockbackListener.java b/src/main/java/uk/antiperson/stackmob/listeners/KnockbackListener.java
new file mode 100644
index 00000000..e8705c74
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/listeners/KnockbackListener.java
@@ -0,0 +1,35 @@
+package uk.antiperson.stackmob.listeners;
+
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityKnockbackEvent;
+import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.entity.StackEntity;
+
+@ListenerMetadata(config = "disable-knockback.enabled")
+public class KnockbackListener implements Listener {
+
+ private final StackMob sm;
+ public KnockbackListener(StackMob sm) {
+ this.sm = sm;
+ }
+
+ @EventHandler
+ public void onEntityKnockback(EntityKnockbackEvent event) {
+ StackEntity stackEntity = sm.getEntityManager().getStackEntity(event.getEntity());
+ if (stackEntity == null) {
+ return;
+ }
+ if (!stackEntity.getEntityConfig().isKnockbackDisabledTypes()) {
+ return;
+ }
+ if (!stackEntity.getEntityConfig().isKnockbackDisabledReasons(event.getEntity().getEntitySpawnReason())) {
+ return;
+ }
+ if (!stackEntity.getEntityConfig().isKnockbackDisabledCause(event.getCause())) {
+ return;
+ }
+ event.setCancelled(true);
+ }
+
+}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/LeashListener.java b/src/main/java/uk/antiperson/stackmob/listeners/LeashListener.java
new file mode 100644
index 00000000..fd3cb8c0
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/listeners/LeashListener.java
@@ -0,0 +1,29 @@
+package uk.antiperson.stackmob.listeners;
+
+import org.bukkit.Material;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityDropItemEvent;
+import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.utils.Utilities;
+
+@ListenerMetadata(config = "traits.leashed")
+public class LeashListener implements Listener {
+
+ private final StackMob sm;
+ public LeashListener(StackMob sm) {
+ this.sm = sm;
+ }
+
+ @EventHandler
+ public void onLeash(EntityDropItemEvent event) {
+ if (event.getItemDrop().getItemStack().getType() != Material.LEAD) {
+ return;
+ }
+ if (!event.getEntity().hasMetadata(Utilities.NO_LEASH_METADATA)) {
+ return;
+ }
+ event.getEntity().removeMetadata(Utilities.NO_LEASH_METADATA, sm);
+ event.setCancelled(true);
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/ListenerMode.java b/src/main/java/uk/antiperson/stackmob/listeners/ListenerMode.java
deleted file mode 100644
index 09bdb0f2..00000000
--- a/src/main/java/uk/antiperson/stackmob/listeners/ListenerMode.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package uk.antiperson.stackmob.listeners;
-
-public enum ListenerMode {
- MULTIPLY,
- SPLIT
-}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/PlayerListener.java b/src/main/java/uk/antiperson/stackmob/listeners/PlayerListener.java
index 44944fec..fac18470 100644
--- a/src/main/java/uk/antiperson/stackmob/listeners/PlayerListener.java
+++ b/src/main/java/uk/antiperson/stackmob/listeners/PlayerListener.java
@@ -7,7 +7,6 @@
import org.bukkit.event.player.PlayerInteractAtEntityEvent;
import org.bukkit.inventory.EquipmentSlot;
import uk.antiperson.stackmob.StackMob;
-import uk.antiperson.stackmob.entity.StackEntity;
import uk.antiperson.stackmob.utils.StackingTool;
public class PlayerListener implements Listener {
@@ -28,15 +27,11 @@ public void onPlayerInteract(PlayerInteractAtEntityEvent event) {
if (!sm.getItemTools().isStackingTool(event.getPlayer().getInventory().getItemInMainHand())) {
return;
}
- if (!sm.getEntityManager().isStackedEntity((LivingEntity) event.getRightClicked())) {
- return;
- }
StackingTool stackingTool = new StackingTool(sm, event.getPlayer());
if (event.getPlayer().isSneaking()) {
stackingTool.shiftMode();
return;
}
- StackEntity stackEntity = sm.getEntityManager().getStackEntity((LivingEntity) event.getRightClicked());
- stackingTool.performAction(stackEntity);
+ stackingTool.performAction((LivingEntity) event.getRightClicked());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/RemoveListener.java b/src/main/java/uk/antiperson/stackmob/listeners/RemoveListener.java
new file mode 100644
index 00000000..11bb2e8a
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/listeners/RemoveListener.java
@@ -0,0 +1,46 @@
+package uk.antiperson.stackmob.listeners;
+
+import com.destroystokyo.paper.event.entity.EntityAddToWorldEvent;
+import com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent;
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.entity.Mob;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.entity.StackEntity;
+
+public class RemoveListener implements Listener {
+
+ private final StackMob sm;
+ public RemoveListener(StackMob sm) {
+ this.sm = sm;
+ }
+
+ @EventHandler
+ public void onEntityRemove(EntityRemoveFromWorldEvent event) {
+ if (!(event.getEntity() instanceof Mob)) {
+ return;
+ }
+ LivingEntity livingEntity = (LivingEntity) event.getEntity();
+ StackEntity stackEntity = sm.getEntityManager().getStackEntity(livingEntity);
+ if (stackEntity == null) {
+ return;
+ }
+ if (stackEntity.isRemoved()) {
+ return;
+ }
+ sm.getEntityManager().unregisterStackedEntity(stackEntity);
+ }
+
+ @EventHandler
+ public void onEntityAdd(EntityAddToWorldEvent event) {
+ if (!(event.getEntity() instanceof Mob)) {
+ return;
+ }
+ LivingEntity livingEntity = (LivingEntity) event.getEntity();
+ if (!sm.getEntityManager().hasStackData(livingEntity)) {
+ return;
+ }
+ sm.getEntityManager().registerStackedEntity(livingEntity);
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/ShearListener.java b/src/main/java/uk/antiperson/stackmob/listeners/ShearListener.java
index 495b8d0c..eda08ef3 100644
--- a/src/main/java/uk/antiperson/stackmob/listeners/ShearListener.java
+++ b/src/main/java/uk/antiperson/stackmob/listeners/ShearListener.java
@@ -1,18 +1,20 @@
package uk.antiperson.stackmob.listeners;
import org.bukkit.Material;
+import org.bukkit.Tag;
import org.bukkit.block.Dispenser;
import org.bukkit.entity.*;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockShearEntityEvent;
import org.bukkit.event.player.PlayerShearEntityEvent;
+import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.loot.LootContext;
-import org.bukkit.material.Wool;
import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.config.EntityConfig;
import uk.antiperson.stackmob.entity.Drops;
import uk.antiperson.stackmob.entity.StackEntity;
@@ -33,11 +35,16 @@ public void onShearSheep(PlayerShearEntityEvent event) {
if (event.isCancelled()) {
return;
}
- ItemStack is = shearLogic((LivingEntity) event.getEntity(), event.getPlayer().getInventory().getItemInMainHand());
+ EquipmentSlot equipmentSlot = findShears(event.getPlayer());
+ if (equipmentSlot == null) {
+ sm.getLogger().info("A player just managed to shear an entity while not holding shears.");
+ return;
+ }
+ ItemStack is = shearLogic((LivingEntity) event.getEntity(), event.getPlayer().getInventory().getItem(equipmentSlot));
if (is == null) {
return;
}
- event.getPlayer().getInventory().setItemInMainHand(is);
+ event.getPlayer().getInventory().setItem(equipmentSlot, is);
}
@EventHandler
@@ -46,63 +53,81 @@ public void onShearSheep(BlockShearEntityEvent event) {
return;
}
ItemStack is = shearLogic((LivingEntity) event.getEntity(), event.getTool());
- if (is == null) {
+ int durability = ((Damageable) event.getTool().getItemMeta()).getDamage();
+ int maxDurability = event.getTool().getType().getMaxDurability();
+ if (is == null || (maxDurability - durability) == 1) {
return;
}
- sm.getServer().getScheduler().runTask(sm, () -> {
+ sm.getScheduler().runTask(event.getBlock().getLocation(), () -> {
Dispenser dispenser = (Dispenser) event.getBlock().getState();
dispenser.getInventory().setItem(dispenser.getInventory().first(event.getTool()), is);
});
}
+ private EquipmentSlot findShears(Player player) {
+ EquipmentSlot hand = checkSlot(player, EquipmentSlot.HAND);
+ EquipmentSlot offHand = checkSlot(player, EquipmentSlot.OFF_HAND);
+ return hand == null ? offHand : hand;
+ }
+
+ private EquipmentSlot checkSlot(Player player, EquipmentSlot slot) {
+ if (player.getInventory().getItem(slot).getType() == Material.SHEARS) {
+ return slot;
+ }
+ return null;
+ }
+
private ItemStack shearLogic(LivingEntity entity, ItemStack item) {
if (!((entity instanceof Sheep) || (entity instanceof MushroomCow))) {
return null;
}
StackEntity stackEntity = sm.getEntityManager().getStackEntity(entity);
- if (stackEntity.isSingle()) {
+ if (stackEntity == null || stackEntity.isSingle()) {
return null;
}
- switch (sm.getMainConfig().getListenerMode(entity.getType(), "shear")) {
- case SPLIT:
- StackEntity slice = stackEntity.slice();
- if (slice.getEntity() instanceof Sheep) {
- ((Sheep) slice.getEntity()).setSheared(false);
- }
- break;
- case MULTIPLY:
- Damageable damageable = (Damageable) item.getItemMeta();
- int health = item.getType().getMaxDurability() - damageable.getDamage();
- stackEntity.splitIfNotEnough(health);
- int damage = health - stackEntity.getSize();
- if (damage > 0) {
- damageable.setDamage(damageable.getDamage() + stackEntity.getSize());
- item.setItemMeta((ItemMeta) damageable);
- } else {
- item = new ItemStack(Material.AIR);
- }
- if (entity instanceof Sheep) {
- Sheep sheared = (Sheep) entity;
- LootContext lootContext = new LootContext.Builder(sheared.getLocation()).lootedEntity(sheared).build();
- Collection loot = sheared.getLootTable().populateLoot(ThreadLocalRandom.current(), lootContext);
- for(ItemStack itemStack : loot){
- if(itemStack.getData() instanceof Wool) {
- int woolAmount = (int) Math.round(stackEntity.getSize() * ThreadLocalRandom.current().nextDouble(1,2));
- Drops.dropItem(sheared.getLocation(), itemStack, woolAmount);
- }
- }
- return item;
+ EntityConfig.ListenerMode shear = stackEntity.getEntityConfig().getListenerMode(EntityConfig.EventType.SHEAR);
+ if (shear == EntityConfig.ListenerMode.SPLIT) {
+ StackEntity slice = stackEntity.slice();
+ if (slice.getEntity() instanceof Sheep) {
+ ((Sheep) slice.getEntity()).setSheared(false);
+ }
+ return null;
+ }
+ int limit = stackEntity.getEntityConfig().getEventMultiplyLimit(EntityConfig.EventType.SHEAR, stackEntity.getSize());
+ Damageable damageable = (Damageable) item.getItemMeta();
+ int health = item.getType().getMaxDurability() - damageable.getDamage();
+ int amount = Math.min(health, limit);
+ stackEntity.splitIfNotEnough(amount);
+ int damage = health - amount;
+ if (damage > 0) {
+ damageable.setDamage(damageable.getDamage() + amount);
+ item.setItemMeta((ItemMeta) damageable);
+ } else {
+ item = new ItemStack(Material.AIR);
+ }
+ if (entity instanceof Sheep) {
+ Sheep sheared = (Sheep) entity;
+ LootContext lootContext = new LootContext.Builder(sheared.getLocation()).lootedEntity(sheared).build();
+ Collection loot = sheared.getLootTable().populateLoot(ThreadLocalRandom.current(), lootContext);
+ for (ItemStack itemStack : loot) {
+ if (Tag.WOOL.isTagged(itemStack.getType())) {
+ int woolAmount = (int) Math.round(amount * ThreadLocalRandom.current().nextDouble(1, 2));
+ Drops.dropItem(sheared.getLocation(), itemStack, woolAmount, true);
}
- MushroomCow mushroomCow = (MushroomCow) entity;
- ItemStack mushrooms = new ItemStack(Material.RED_MUSHROOM,1);
- Drops.dropItem(mushroomCow.getLocation(), mushrooms, (stackEntity.getSize() - 1) * 5);
- // Spawn separate normal cow for the rest of the stack.
- Entity cow = mushroomCow.getWorld().spawnEntity(mushroomCow.getLocation(), EntityType.COW);
- StackEntity stackCow = sm.getEntityManager().getStackEntity((LivingEntity) cow);
- stackCow.setSize(stackEntity.getSize() - 1);
- return item;
+ }
+ return item;
}
- return null;
+ MushroomCow mushroomCow = (MushroomCow) entity;
+ ItemStack mushrooms = new ItemStack(getMaterial(mushroomCow), 1);
+ Drops.dropItem(mushroomCow.getLocation(), mushrooms, (amount - 1) * 5, true);
+ // Spawn separate normal cow for the rest of the stack.
+ Entity cow = mushroomCow.getWorld().spawnEntity(mushroomCow.getLocation(), EntityType.COW);
+ StackEntity stackCow = sm.getEntityManager().registerStackedEntity((LivingEntity) cow);
+ stackCow.setSize(amount - 1);
+ return item;
}
+ private Material getMaterial(MushroomCow mushroomCow) {
+ return mushroomCow.getVariant() == MushroomCow.Variant.RED ? Material.RED_MUSHROOM : Material.BROWN_MUSHROOM;
+ }
}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/SlimeListener.java b/src/main/java/uk/antiperson/stackmob/listeners/SlimeListener.java
index 4e9bd457..70c628ca 100644
--- a/src/main/java/uk/antiperson/stackmob/listeners/SlimeListener.java
+++ b/src/main/java/uk/antiperson/stackmob/listeners/SlimeListener.java
@@ -19,8 +19,11 @@ public SlimeListener(StackMob sm) {
@EventHandler
public void onSlimeSplit(SlimeSplitEvent event) {
+ if (!sm.getEntityManager().isStackedEntity(event.getEntity())) {
+ return;
+ }
StackEntity stackEntity = sm.getEntityManager().getStackEntity(event.getEntity());
- if (stackEntity.isSingle()) {
+ if (stackEntity == null || stackEntity.isSingle()) {
return;
}
if (!event.getEntity().hasMetadata(Utilities.SLIME_METADATA)) {
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/SpawnListener.java b/src/main/java/uk/antiperson/stackmob/listeners/SpawnListener.java
index 1a18527e..c51628b9 100644
--- a/src/main/java/uk/antiperson/stackmob/listeners/SpawnListener.java
+++ b/src/main/java/uk/antiperson/stackmob/listeners/SpawnListener.java
@@ -1,13 +1,16 @@
package uk.antiperson.stackmob.listeners;
+import org.bukkit.entity.Bee;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Mob;
+import org.bukkit.entity.ZombieVillager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import uk.antiperson.stackmob.StackMob;
import uk.antiperson.stackmob.entity.StackEntity;
+import uk.antiperson.stackmob.events.EventHelper;
public class SpawnListener implements Listener {
@@ -21,36 +24,62 @@ public void onSpawn(CreatureSpawnEvent event) {
if (!(event.getEntity() instanceof Mob)) {
return;
}
- sm.getServer().getScheduler().runTask(sm, () -> {
- if (sm.getMainConfig().isEntityBlacklisted(event.getEntity(), event.getSpawnReason())) {
+ if (event.getEntity() instanceof Bee) {
+ if (event.getSpawnReason() == CreatureSpawnEvent.SpawnReason.BEEHIVE) {
return;
}
+ }
+ sm.getScheduler().runTask(event.getEntity(), () -> {
if (sm.getEntityManager().isStackedEntity(event.getEntity())) {
+ StackEntity stackEntity = sm.getEntityManager().getStackEntity(event.getEntity());
+ if (stackEntity != null && stackEntity.isForgetOnSpawn()) {
+ stackEntity.removeStackData();
+ }
return;
}
- StackEntity original = sm.getEntityManager().getStackEntity(event.getEntity());
+ if (sm.getMainConfig().getConfig(event.getEntity().getType()).isEntityBlacklisted(event.getEntity(), event.getSpawnReason())) {
+ return;
+ }
+ if (sm.getHookManager().spawnCheck(event.getEntity())) {
+ return;
+ }
+ if (EventHelper.callStackSpawnEvent(event.getEntity()).isCancelled()) {
+ return;
+ }
+ StackEntity original = sm.getEntityManager().registerStackedEntity(event.getEntity());
if (original.shouldWait(event.getSpawnReason())) {
original.makeWait();
return;
}
- Integer[] searchRadius = sm.getMainConfig().getStackRadius(event.getEntity().getType());
+ sm.getHookManager().onSpawn(original);
+ original.setSize(1);
+ if (!original.getEntityConfig().isStackOnSpawn()) {
+ return;
+ }
+ Integer[] searchRadius = original.getEntityConfig().getStackRadius();
for (Entity entity : event.getEntity().getNearbyEntities(searchRadius[0], searchRadius[1], searchRadius[2])) {
if (!(entity instanceof Mob)) {
continue;
}
StackEntity nearby = sm.getEntityManager().getStackEntity((LivingEntity) entity);
- if (sm.getMainConfig().getStackThresholdEnabled(entity.getType()) && nearby.getSize() == 1) {
+ if (nearby == null) {
+ continue;
+ }
+ if (!nearby.canStack()) {
continue;
}
- if (!original.checkNearby(nearby)) {
+ if (!original.match(nearby)) {
continue;
}
- if (nearby.merge(original)) {
+ if (original.getEntityConfig().getStackThresholdEnabled() && nearby.getSize() == 1) {
+ continue;
+ }
+ StackEntity removed = nearby.merge(original, true);
+ if (removed != null) {
+ removed.removeStackData();
return;
}
}
- original.setSize(1);
- sm.getHookManager().onSpawn(original);
});
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/TagInteractListener.java b/src/main/java/uk/antiperson/stackmob/listeners/TagInteractListener.java
index 9ca9509b..7fe2109d 100644
--- a/src/main/java/uk/antiperson/stackmob/listeners/TagInteractListener.java
+++ b/src/main/java/uk/antiperson/stackmob/listeners/TagInteractListener.java
@@ -9,9 +9,10 @@
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.config.EntityConfig;
import uk.antiperson.stackmob.entity.StackEntity;
-@ListenerMetadata(config = "events.divide.nametag")
+@ListenerMetadata(config = "events.nametag.enabled")
public class TagInteractListener implements Listener {
private final StackMob sm;
@@ -35,12 +36,20 @@ public void onTagInteract(PlayerInteractEntityEvent event) {
return;
}
StackEntity stackEntity = sm.getEntityManager().getStackEntity((LivingEntity) event.getRightClicked());
- if (sm.getMainConfig().removeStackDataOnDivide("nametag")) {
- stackEntity.removeStackData();
- }
- if (stackEntity.isSingle()) {
+ if (stackEntity == null) {
return;
}
- stackEntity.slice();
+ EntityConfig.NameTagInteractMode nameTagInteractMode = stackEntity.getEntityConfig().getNameTagInteractMode();
+ switch (nameTagInteractMode) {
+ case PREVENT:
+ event.setCancelled(true);
+ break;
+ case SLICE:
+ if (!stackEntity.isSingle()) {
+ stackEntity.slice();
+ }
+ stackEntity.removeStackData();
+ break;
+ }
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/TameListener.java b/src/main/java/uk/antiperson/stackmob/listeners/TameListener.java
index 0feb1c46..5957bd58 100644
--- a/src/main/java/uk/antiperson/stackmob/listeners/TameListener.java
+++ b/src/main/java/uk/antiperson/stackmob/listeners/TameListener.java
@@ -17,9 +17,12 @@ public TameListener(StackMob sm) {
@EventHandler
public void onTame(EntityTameEvent event) {
StackEntity stackEntity = sm.getEntityManager().getStackEntity(event.getEntity());
- if (stackEntity.isSingle()) {
+ if (stackEntity == null) {
return;
}
- stackEntity.slice();
+ if (!stackEntity.isSingle()) {
+ stackEntity.slice();
+ }
+ stackEntity.removeStackData();
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/TargetListener.java b/src/main/java/uk/antiperson/stackmob/listeners/TargetListener.java
index fe5758a2..5500e6e8 100644
--- a/src/main/java/uk/antiperson/stackmob/listeners/TargetListener.java
+++ b/src/main/java/uk/antiperson/stackmob/listeners/TargetListener.java
@@ -1,10 +1,12 @@
package uk.antiperson.stackmob.listeners;
-import org.bukkit.entity.*;
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.entity.Monster;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityTargetEvent;
import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.entity.StackEntity;
import uk.antiperson.stackmob.utils.Utilities;
@ListenerMetadata(config = "disable-targeting.enabled")
@@ -23,13 +25,12 @@ public void onMobTarget(EntityTargetEvent event) {
if (!sm.getEntityManager().isStackedEntity((LivingEntity) event.getEntity())){
return;
}
- if (sm.getMainConfig().getTargetingDisabledTypes(event.getEntityType()).contains(event.getEntityType().toString())) {
+ StackEntity stackEntity = sm.getEntityManager().getStackEntity((LivingEntity) event.getEntity());
+ if (stackEntity.getEntityConfig().isTargetingDisabledTypes()) {
return;
}
- if (Utilities.isPaper()) {
- if (sm.getMainConfig().getTargetingDisabledReasons(event.getEntityType()).contains(event.getEntity().getEntitySpawnReason())){
- return;
- }
+ if (Utilities.isPaper() && stackEntity.getEntityConfig().isTargetingDisabledReasons(event.getEntity().getEntitySpawnReason())){
+ return;
}
event.setCancelled(true);
}
diff --git a/src/main/java/uk/antiperson/stackmob/listeners/TransformListener.java b/src/main/java/uk/antiperson/stackmob/listeners/TransformListener.java
index 6b671737..12d7929d 100644
--- a/src/main/java/uk/antiperson/stackmob/listeners/TransformListener.java
+++ b/src/main/java/uk/antiperson/stackmob/listeners/TransformListener.java
@@ -16,14 +16,16 @@ public TransformListener(StackMob sm) {
@EventHandler
public void onTransform(EntityTransformEvent event){
- if (event.getTransformReason() != EntityTransformEvent.TransformReason.DROWNED) {
+ if (event.getTransformReason() == EntityTransformEvent.TransformReason.SHEARED ||
+ event.getTransformReason() == EntityTransformEvent.TransformReason.SPLIT) {
return;
}
- if (!sm.getEntityManager().isStackedEntity(((LivingEntity) event.getEntity()))) {
+ StackEntity stackEntity = sm.getEntityManager().getStackEntity((LivingEntity) event.getEntity());
+ StackEntity transformed = sm.getEntityManager().registerStackedEntity((LivingEntity) event.getTransformedEntity());
+ if (stackEntity == null) {
+ transformed.setForgetOnSpawn(true);
return;
}
- StackEntity stackEntity = sm.getEntityManager().getStackEntity((LivingEntity) event.getEntity());
- StackEntity transformed = sm.getEntityManager().getStackEntity((LivingEntity) event.getTransformedEntity());
transformed.setSize(stackEntity.getSize());
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/scheduler/BukkitScheduler.java b/src/main/java/uk/antiperson/stackmob/scheduler/BukkitScheduler.java
new file mode 100644
index 00000000..5247715e
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/scheduler/BukkitScheduler.java
@@ -0,0 +1,38 @@
+package uk.antiperson.stackmob.scheduler;
+
+import org.bukkit.Location;
+import org.bukkit.entity.Entity;
+import org.bukkit.plugin.Plugin;
+
+public class BukkitScheduler implements Scheduler {
+
+ private final Plugin plugin;
+ public BukkitScheduler(Plugin plugin) {
+ this.plugin = plugin;
+ }
+
+ @Override
+ public void runGlobalTaskTimer(Runnable runnable, long delay, long period) {
+ plugin.getServer().getScheduler().runTaskTimer(plugin, runnable, delay, period);
+ }
+
+ @Override
+ public void runTask(Location location, Runnable runnable) {
+ plugin.getServer().getScheduler().runTask(plugin, runnable);
+ }
+
+ @Override
+ public void runTask(Entity entity, Runnable runnable) {
+ plugin.getServer().getScheduler().runTask(plugin, runnable);
+ }
+
+ @Override
+ public void runTaskAsynchronously(Runnable runnable) {
+ plugin.getServer().getScheduler().runTaskAsynchronously(plugin, runnable);
+ }
+
+ @Override
+ public void runTaskLater(Entity entity, Runnable runnable, long delay) {
+ plugin.getServer().getScheduler().runTaskLater(plugin, runnable, delay);
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/scheduler/FoliaScheduler.java b/src/main/java/uk/antiperson/stackmob/scheduler/FoliaScheduler.java
new file mode 100644
index 00000000..e0643927
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/scheduler/FoliaScheduler.java
@@ -0,0 +1,39 @@
+package uk.antiperson.stackmob.scheduler;
+
+import org.bukkit.Location;
+import org.bukkit.entity.Entity;
+import org.bukkit.plugin.Plugin;
+
+public class FoliaScheduler implements Scheduler {
+
+ private final Plugin plugin;
+
+ public FoliaScheduler(Plugin plugin) {
+ this.plugin = plugin;
+ }
+
+ @Override
+ public void runGlobalTaskTimer(Runnable runnable, long delay, long period) {
+ plugin.getServer().getGlobalRegionScheduler().runAtFixedRate(plugin, scheduledTask -> runnable.run(), (int) delay, (int) period);
+ }
+
+ @Override
+ public void runTask(Location location, Runnable runnable) {
+ plugin.getServer().getRegionScheduler().run(plugin, location, scheduledTask -> runnable.run());
+ }
+
+ @Override
+ public void runTask(Entity entity, Runnable runnable) {
+ entity.getScheduler().run(plugin, scheduledTask -> runnable.run(), () -> {});
+ }
+
+ @Override
+ public void runTaskAsynchronously(Runnable runnable) {
+ plugin.getServer().getAsyncScheduler().runNow(plugin, scheduledTask -> runnable.run());
+ }
+
+ @Override
+ public void runTaskLater(Entity entity, Runnable runnable, long delay) {
+ entity.getScheduler().runDelayed(plugin, scheduledTask -> runnable.run(), () -> {}, (int) delay);
+ }
+}
diff --git a/src/main/java/uk/antiperson/stackmob/scheduler/Scheduler.java b/src/main/java/uk/antiperson/stackmob/scheduler/Scheduler.java
new file mode 100644
index 00000000..b50803bb
--- /dev/null
+++ b/src/main/java/uk/antiperson/stackmob/scheduler/Scheduler.java
@@ -0,0 +1,13 @@
+package uk.antiperson.stackmob.scheduler;
+
+import org.bukkit.Location;
+import org.bukkit.entity.Entity;
+import org.bukkit.plugin.Plugin;
+
+public interface Scheduler {
+ void runGlobalTaskTimer(Runnable runnable, long delay, long period);
+ void runTask(Location location, Runnable runnable);
+ void runTask(Entity entity, Runnable runnable);
+ void runTaskAsynchronously(Runnable runnable);
+ void runTaskLater(Entity entity, Runnable runnable, long delay);
+}
diff --git a/src/main/java/uk/antiperson/stackmob/tasks/MergeTask.java b/src/main/java/uk/antiperson/stackmob/tasks/MergeTask.java
index 855ec3fa..2debc0ea 100644
--- a/src/main/java/uk/antiperson/stackmob/tasks/MergeTask.java
+++ b/src/main/java/uk/antiperson/stackmob/tasks/MergeTask.java
@@ -1,81 +1,112 @@
package uk.antiperson.stackmob.tasks;
-import org.bukkit.Bukkit;
-import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Mob;
-import org.bukkit.scheduler.BukkitRunnable;
import uk.antiperson.stackmob.StackMob;
import uk.antiperson.stackmob.entity.StackEntity;
+import uk.antiperson.stackmob.utils.Utilities;
import java.util.HashSet;
import java.util.Set;
-public class MergeTask extends BukkitRunnable {
+public class MergeTask implements Runnable {
private final StackMob sm;
+
public MergeTask(StackMob sm) {
this.sm = sm;
}
- public void run() {
- for (World world : Bukkit.getWorlds()) {
- for (LivingEntity entity : world.getEntitiesByClass(Mob.class)) {
- StackEntity original = sm.getEntityManager().getStackEntity(entity);
- if (!sm.getEntityManager().isStackedEntity(entity)) {
- if (sm.getEntityManager().isWaiting(original.getEntity())) {
- original.incrementWait();
- }
- continue;
- }
- if (original.isMaxSize()) {
- continue;
- }
- Integer[] searchRadius = sm.getMainConfig().getStackRadius(entity.getType());
- Set matches = new HashSet<>();
- for (Entity nearby : entity.getNearbyEntities(searchRadius[0], searchRadius[1], searchRadius[2])) {
- if (!(nearby instanceof Mob)) {
- continue;
- }
- if (!sm.getEntityManager().isStackedEntity((LivingEntity) nearby)) {
- continue;
- }
- StackEntity nearbyStack = sm.getEntityManager().getStackEntity((LivingEntity) nearby);
- if (!original.checkNearby(nearbyStack)) {
- continue;
- }
- if (nearbyStack.getSize() > 1 || original.getSize() > 1) {
- if (nearbyStack.merge(original)) {
- break;
- }
+ private void checkEntity(StackEntity original, boolean checkHasMoved, double checkHasMovedDistance) {
+ if (original.isWaiting()) {
+ original.incrementWait();
+ return;
+ }
+ if (!original.canStack()) {
+ if (!original.getEntity().isValid()) {
+ removeEntity(original);
+ }
+ return;
+ }
+ if (checkHasMoved) {
+ if (original.getEntity().getWorld().equals(original.getLastLocation().getWorld())) {
+ if (!original.skipLastLocation()) {
+ if (original.getEntity().getLocation().distance(original.getLastLocation()) < checkHasMovedDistance) {
+ return;
}
- matches.add(nearbyStack);
- }
- if (!sm.getMainConfig().getStackThresholdEnabled(entity.getType())) {
- continue;
}
- int threshold = sm.getMainConfig().getStackThreshold(entity.getType()) - 1;
- int size = matches.size();
- if (size < threshold) {
- continue;
- }
- matches.forEach(StackEntity::remove);
- if (size >= original.getMaxSize()) {
- double divided = (double) size / (double) original.getMaxSize();
- double fullStacks = Math.floor(divided);
- double leftOver = divided - fullStacks;
- for (int i = 0; i < fullStacks; i++) {
- StackEntity stackEntity = original.duplicate();
- stackEntity.setSize(original.getMaxSize());
- }
- if (leftOver > 0) {
- StackEntity stackEntity = original.duplicate();
- stackEntity.setSize((int) Math.round(leftOver * original.getMaxSize()));
+ }
+ original.setLastLocation(original.getEntity().getLocation());
+ }
+ boolean stackThresholdEnabled = original.getEntityConfig().getStackThresholdEnabled();
+ Integer[] searchRadius = original.getEntityConfig().getStackRadius();
+ Set matches = new HashSet<>();
+ for (Entity nearby : original.getEntity().getNearbyEntities(searchRadius[0], searchRadius[1], searchRadius[2])) {
+ if (!(nearby instanceof Mob)) {
+ continue;
+ }
+ StackEntity nearbyStack = sm.getEntityManager().getStackEntity((LivingEntity) nearby);
+ if (nearbyStack == null) {
+ continue;
+ }
+ if (!nearbyStack.canStack()) {
+ continue;
+ }
+ if (!original.match(nearbyStack)) {
+ continue;
+ }
+ if (!stackThresholdEnabled || (nearbyStack.getSize() > 1 || original.getSize() > 1)) {
+ final StackEntity removed = nearbyStack.merge(original, false);
+ if (removed != null) {
+ removeEntity(removed);
+ if (original == removed) {
+ return;
}
- return;
+ break;
}
- original.incrementSize(size);
+ continue;
+ }
+ matches.add(nearbyStack);
+ }
+ if (!stackThresholdEnabled) {
+ return;
+ }
+ int threshold = original.getEntityConfig().getStackThreshold() - 1;
+ int size = matches.size();
+ if (size < threshold) {
+ return;
+ }
+ for (StackEntity match : matches) {
+ match.remove(false);
+ removeEntity(match);
+ }
+ if (size + original.getSize() > original.getMaxSize()) {
+ final int toCompleteStack = (original.getMaxSize() - original.getSize());
+ original.incrementSize(toCompleteStack);
+ for (int stackSize : Utilities.split(size - toCompleteStack, original.getMaxSize())) {
+ StackEntity stackEntity = original.duplicate();
+ stackEntity.setSize(stackSize);
+ }
+ return;
+ }
+ original.incrementSize(size);
+ }
+
+ private void removeEntity(StackEntity stackEntity) {
+ sm.getEntityManager().unregisterStackedEntity(stackEntity);
+ }
+
+ @Override
+ public void run() {
+ boolean checkHasMoved = sm.getMainConfig().getConfig().isCheckHasMoved();
+ double checkHasMovedDistance = sm.getMainConfig().getConfig().getCheckHasMovedDistance();
+ for (StackEntity original : sm.getEntityManager().getStackEntities()) {
+ Runnable runnable = () -> checkEntity(original, checkHasMoved, checkHasMovedDistance);
+ if (Utilities.IS_FOLIA) {
+ sm.getScheduler().runTask(original.getEntity(), runnable);
+ } else {
+ runnable.run();
}
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/tasks/TagTask.java b/src/main/java/uk/antiperson/stackmob/tasks/TagTask.java
deleted file mode 100644
index 7c82bc78..00000000
--- a/src/main/java/uk/antiperson/stackmob/tasks/TagTask.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package uk.antiperson.stackmob.tasks;
-
-import org.bukkit.Bukkit;
-import org.bukkit.entity.Entity;
-import org.bukkit.entity.LivingEntity;
-import org.bukkit.entity.Mob;
-import org.bukkit.entity.Player;
-import org.bukkit.scheduler.BukkitRunnable;
-import uk.antiperson.stackmob.StackMob;
-import uk.antiperson.stackmob.entity.StackEntity;
-import uk.antiperson.stackmob.entity.TagMode;
-
-import java.util.List;
-
-public class TagTask extends BukkitRunnable {
-
- private final StackMob sm;
- public TagTask(StackMob sm) {
- this.sm = sm;
- }
-
- @Override
- public void run() {
- Integer[] searchRadius = sm.getMainConfig().getTagNeabyRadius();
- double searchX = searchRadius[0];
- double searchY = searchRadius[1];
- double searchZ = searchRadius[2];
- for (Player player : Bukkit.getOnlinePlayers()) {
- List entities = player.getNearbyEntities(searchX * 1.5, searchY * 1.5, searchZ * 1.5);
- for (Entity entity : entities) {
- if (!(entity instanceof Mob)) {
- continue;
- }
- if (!sm.getEntityManager().isStackedEntity((LivingEntity) entity)) {
- continue;
- }
- if (entity.isDead()) {
- continue;
- }
- if (sm.getMainConfig().getTagMode(entity.getType()) != TagMode.NEARBY) {
- return;
- }
- StackEntity stackEntity = sm.getEntityManager().getStackEntity((LivingEntity) entity);
- int threshold = sm.getMainConfig().getTagThreshold(stackEntity.getEntity().getType());
- if (stackEntity.getSize() <= threshold) {
- return;
- }
- double xDiff = Math.abs(player.getLocation().getX() - entity.getLocation().getX());
- double yDiff = Math.abs(player.getLocation().getY() - entity.getLocation().getY());
- double zDiff = Math.abs(player.getLocation().getZ() - entity.getLocation().getZ());
- if (xDiff < searchX && yDiff < searchY && zDiff < searchZ) {
- // Player should be shown tag
- stackEntity.getTag().sendPacket(player, true);
- continue;
- }
- // Player should not be shown tag
- stackEntity.getTag().sendPacket(player, false);
- }
- }
- }
-
-}
diff --git a/src/main/java/uk/antiperson/stackmob/utils/EntityUtils.java b/src/main/java/uk/antiperson/stackmob/utils/EntityUtils.java
deleted file mode 100644
index 5bf705a3..00000000
--- a/src/main/java/uk/antiperson/stackmob/utils/EntityUtils.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package uk.antiperson.stackmob.utils;
-
-import org.bukkit.Material;
-import org.bukkit.Tag;
-import org.bukkit.entity.*;
-import org.bukkit.inventory.ItemStack;
-
-public class EntityUtils {
-
-
- public static boolean isCorrectFood(Entity entity, Material type) {
- switch (entity.getType()) {
- case COW:
- case SHEEP:
- case MUSHROOM_COW:
- return type == Material.WHEAT;
- case PIG:
- return (type == Material.CARROT || type == Material.BEETROOT || type == Material.POTATO);
- case CHICKEN:
- return type == Material.WHEAT_SEEDS
- || type == Material.MELON_SEEDS
- || type == Material.BEETROOT_SEEDS
- || type == Material.PUMPKIN_SEEDS;
- case HORSE:
- return (type == Material.GOLDEN_APPLE || type == Material.GOLDEN_CARROT) && ((Horse)entity).isTamed();
- case WOLF:
- return (type == Material.BEEF
- || type == Material.CHICKEN
- || type == Material.COD
- || type == Material.MUTTON
- || type == Material.PORKCHOP
- || type == Material.RABBIT
- || type == Material.SALMON
- || type == Material.COOKED_BEEF
- || type == Material.COOKED_CHICKEN
- || type == Material.COOKED_COD
- || type == Material.COOKED_MUTTON
- || type == Material.COOKED_PORKCHOP
- || type == Material.COOKED_RABBIT
- || type == Material.COOKED_SALMON)
- && ((Wolf) entity).isTamed();
- case OCELOT:
- return (type == Material.SALMON
- || type == Material.COD
- || type == Material.PUFFERFISH
- || type == Material.TROPICAL_FISH);
- case RABBIT:
- return type == Material.CARROT || type == Material.GOLDEN_CARROT || type == Material.DANDELION;
- case LLAMA:
- return type == Material.HAY_BLOCK;
- case TURTLE:
- return type == Material.SEAGRASS;
- case PANDA:
- return type == Material.BAMBOO;
- case FOX:
- return type == Material.SWEET_BERRIES;
- case CAT:
- return (type == Material.COD || type == Material.SALMON) && ((Cat) entity).isTamed();
- case BEE:
- return Tag.FLOWERS.isTagged(type);
- }
- if (Utilities.isNewBukkit()) {
- return entity.getType() == EntityType.HOGLIN && type == Material.CRIMSON_FUNGUS;
- }
- return false;
- }
-
- public static boolean isDye(ItemStack material) {
- return material.getType().toString().endsWith("_DYE");
- }
-
- public static void removeHandItem(Player player, int itemAmount) {
- if (itemAmount == player.getInventory().getItemInMainHand().getAmount()) {
- player.getInventory().setItemInMainHand(null);
- return;
- }
- ItemStack is = player.getInventory().getItemInMainHand();
- is.setAmount(is.getAmount() - itemAmount);
- player.getInventory().setItemInMainHand(is);
- }
-
-
-}
diff --git a/src/main/java/uk/antiperson/stackmob/utils/ItemTools.java b/src/main/java/uk/antiperson/stackmob/utils/ItemTools.java
index cfb4a8d8..b29395bf 100644
--- a/src/main/java/uk/antiperson/stackmob/utils/ItemTools.java
+++ b/src/main/java/uk/antiperson/stackmob/utils/ItemTools.java
@@ -10,9 +10,15 @@
import uk.antiperson.stackmob.StackMob;
import java.util.Arrays;
+import java.util.List;
public class ItemTools {
+ public static final String ITEM_NAME = ChatColor.GOLD + "The Stick Of Stacking";
+ public static final List ITEM_LORE = Arrays.asList(ChatColor.GREEN + "A useful tool for modifying stacked mobs.",
+ ChatColor.GOLD + "Right click to perform action" ,
+ ChatColor.GOLD + "Shift-right click to change mode.");
+
private final StackMob sm;
public ItemTools(StackMob sm) {
this.sm = sm;
@@ -20,12 +26,10 @@ public ItemTools(StackMob sm) {
public ItemStack createStackingTool() {
ItemStack is = new ItemStack(Material.BONE, 1);
- is.addUnsafeEnchantment(Enchantment.ARROW_KNOCKBACK, 100);
+ is.addUnsafeEnchantment(Enchantment.CHANNELING, 100);
ItemMeta itemMeta = is.getItemMeta();
- itemMeta.setDisplayName(ChatColor.GOLD + "The Stick Of Stacking");
- itemMeta.setLore(Arrays.asList(ChatColor.GREEN + "A useful tool for modifying stacked mobs.",
- ChatColor.GOLD + "Right click to perform action" ,
- ChatColor.GOLD + "Shift-right click to change mode."));
+ itemMeta.setDisplayName(ITEM_NAME);
+ itemMeta.setLore(ITEM_LORE);
itemMeta.getPersistentDataContainer().set(sm.getToolKey(), PersistentDataType.INTEGER, 1);
is.setItemMeta(itemMeta);
return is;
diff --git a/src/main/java/uk/antiperson/stackmob/utils/NMSHelper.java b/src/main/java/uk/antiperson/stackmob/utils/NMSHelper.java
deleted file mode 100644
index 28af8787..00000000
--- a/src/main/java/uk/antiperson/stackmob/utils/NMSHelper.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package uk.antiperson.stackmob.utils;
-
-import net.minecraft.server.v1_16_R1.DataWatcher;
-import net.minecraft.server.v1_16_R1.DataWatcherObject;
-import net.minecraft.server.v1_16_R1.PacketPlayOutEntityMetadata;
-import org.bukkit.craftbukkit.v1_16_R1.entity.CraftEntity;
-import org.bukkit.craftbukkit.v1_16_R1.entity.CraftPlayer;
-import org.bukkit.entity.Entity;
-import org.bukkit.entity.Player;
-
-import java.lang.reflect.Field;
-
-public class NMSHelper {
-
- public static void sendPacket(Player player, Entity entity, boolean tagVisible) throws NoSuchFieldException, IllegalAccessException {
- CraftEntity craftEntity = (CraftEntity) entity;
- Field field = net.minecraft.server.v1_16_R1.Entity.class.getDeclaredField("ay");
- field.setAccessible(true);
- DataWatcherObject datawatcherobject = (DataWatcherObject) field.get(null);
- DataWatcher watcher = craftEntity.getHandle().getDataWatcher();
- watcher.set(datawatcherobject, tagVisible);
- PacketPlayOutEntityMetadata packet = new PacketPlayOutEntityMetadata(craftEntity.getHandle().getId(), watcher, false);
- ((CraftPlayer) player).getHandle().playerConnection.sendPacket(packet);
- }
-}
diff --git a/src/main/java/uk/antiperson/stackmob/utils/StackingTool.java b/src/main/java/uk/antiperson/stackmob/utils/StackingTool.java
index 8d4da0b2..8e6179b6 100644
--- a/src/main/java/uk/antiperson/stackmob/utils/StackingTool.java
+++ b/src/main/java/uk/antiperson/stackmob/utils/StackingTool.java
@@ -1,8 +1,13 @@
package uk.antiperson.stackmob.utils;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.TextColor;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ChatMessageType;
+import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
-import org.bukkit.ChatColor;
+import org.bukkit.Color;
import org.bukkit.conversations.*;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
@@ -14,13 +19,19 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import uk.antiperson.stackmob.StackMob;
+import uk.antiperson.stackmob.commands.User;
import uk.antiperson.stackmob.entity.StackEntity;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
public class StackingTool {
private final Player player;
private final ItemStack itemStack;
private final StackMob sm;
+
public StackingTool(StackMob sm, Player player) {
this.sm = sm;
this.player = player;
@@ -32,8 +43,12 @@ public int getModeId() {
}
public ToolMode getMode() {
+ return getMode(getModeId());
+ }
+
+ private ToolMode getMode(int id) {
for (ToolMode t : ToolMode.values()) {
- if (t.getId() == getModeId()) {
+ if (t.ordinal() == id) {
return t;
}
}
@@ -42,83 +57,118 @@ public ToolMode getMode() {
public void shiftMode() {
ItemMeta itemMeta = itemStack.getItemMeta();
- int nextMode = (getModeId() + 1) > ToolMode.values().length ? 1 : getModeId() + 1;
+ int nextMode = (getModeId() + 1) >= ToolMode.values().length ? 0 : getModeId() + 1;
itemMeta.getPersistentDataContainer().set(sm.getToolKey(), PersistentDataType.INTEGER, nextMode);
+ List lore = itemMeta.getLore();
+ lore.set(lore.size() - 1, ChatColor.of("#FF6347") + "Mode: " + getMode(nextMode));
+ itemMeta.setLore(lore);
itemStack.setItemMeta(itemMeta);
player.getInventory().setItemInMainHand(itemStack);
- player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText("Shifted mode to " + getMode()));
+ Component component = Component.text("Shifted mode to ").color(TextColor.color(211, 211, 211));
+ Component mode = Component.text(getMode(nextMode).toString()).color(TextColor.color(169, 169, 169));
+ player.sendActionBar(component.append(mode));
}
- public void performAction(StackEntity clicked) {
+ public void performAction(LivingEntity clicked) {
+ User user = new User(player, player);
+ if (!sm.getEntityManager().isStackedEntity(clicked)) {
+ if (getMode() != ToolMode.MODIFY) {
+ user.sendError("You cannot use " + getMode() + " on an unstacked entity!");
+ return;
+ }
+ startConversation(clicked);
+ return;
+ }
+ StackEntity stackEntity = sm.getEntityManager().getStackEntity(clicked);
switch (getMode()) {
case MODIFY:
- ConversationFactory factory = new ConversationFactory(sm)
- .withTimeout(25)
- .withFirstPrompt(new ModifyPrompt(clicked))
- .withLocalEcho(false)
- .withPrefix(conversationContext -> Utilities.PREFIX)
- .addConversationAbandonedListener(new ExitPrompt());
- Conversation conversation = factory.buildConversation(player);
- conversation.begin();
- break;
+ startConversation(clicked);
+ return;
case SLICE:
- if (!clicked.isSingle()) {
- clicked.slice();
- clicked.removeStackData();
- break;
+ case SLICE_ALL:
+ if (stackEntity.isSingle()) {
+ user.sendError("Entity is single, therefore it cannot be sliced!");
+ return;
+ }
+ int sliceNo = getMode() == ToolMode.SLICE ? 1 : stackEntity.getSize() - 1;
+ StackEntity slice = stackEntity;
+ Set slices = new HashSet<>();
+ for (int i = 0; i < sliceNo; i++) {
+ slice = slice.slice();
+ slices.add(slice);
+ }
+ stackEntity.removeStackData();
+ if (getMode() == ToolMode.SLICE_ALL) {
+ slices.forEach(sliced -> sliced.setForgetOnSpawn(true));
}
- player.sendMessage("Entity is single so cannot be sliced!");
break;
case REMOVE_CHUNK:
- for (Entity e : clicked.getEntity().getChunk().getEntities()) {
+ for (Entity e : clicked.getChunk().getEntities()) {
if (!(e instanceof Mob)) {
continue;
}
if (!sm.getEntityManager().isStackedEntity((LivingEntity) e)) {
continue;
}
- StackEntity stackEntity = sm.getEntityManager().getStackEntity((LivingEntity) e);
- stackEntity.removeStackData();
+ StackEntity nearby = sm.getEntityManager().getStackEntity((LivingEntity) e);
+ nearby.removeStackData();
}
break;
case REMOVE_SINGLE:
- clicked.removeStackData();
+ stackEntity.removeStackData();
break;
+ case INFO:
+ user.sendInfo("Stack information: ");
+ user.sendRawMessage("Stack size: " + stackEntity.getSize() + " Max size: " + stackEntity.getMaxSize() + " Waiting count: " + stackEntity.getWaitCount());
+ user.sendRawMessage("Can stack: " + stackEntity.canStack() + " Is blacklisted? " + stackEntity.getEntityConfig().isEntityBlacklisted(stackEntity.getEntity()));
+ return;
}
+ user.sendSuccess("Action performed successfully.");
}
- enum ToolMode {
- MODIFY(1),
- SLICE(2),
- REMOVE_SINGLE(3),
- REMOVE_CHUNK(4);
-
- private final int id;
- ToolMode(int id) {
- this.id = id;
- }
+ private void startConversation(LivingEntity stackEntity) {
+ ConversationFactory factory = new ConversationFactory(sm)
+ .withTimeout(25)
+ .withFirstPrompt(new ModifyPrompt(stackEntity))
+ .withLocalEcho(false)
+ .withPrefix(conversationContext -> Utilities.PREFIX_STRING)
+ .addConversationAbandonedListener(new ExitPrompt());
+ Conversation conversation = factory.buildConversation(player);
+ conversation.begin();
+ }
- public int getId() {
- return id;
- }
+ enum ToolMode {
+ MODIFY,
+ SLICE,
+ SLICE_ALL,
+ REMOVE_SINGLE,
+ REMOVE_CHUNK,
+ INFO
}
private class ModifyPrompt extends NumericPrompt {
- private final StackEntity stackEntity;
- public ModifyPrompt(StackEntity stackEntity) {
- this.stackEntity = stackEntity;
+ private final LivingEntity livingEntity;
+ private final int maxSize;
+
+ public ModifyPrompt(LivingEntity livingEntity) {
+ this.livingEntity = livingEntity;
+ this.maxSize = sm.getMainConfig().getConfig(livingEntity.getType()).getMaxStack();
}
@Nullable
@Override
protected Prompt acceptValidatedInput(@NotNull ConversationContext conversationContext, @NotNull Number number) {
- if (stackEntity.getEntity().isDead()) {
- conversationContext.getForWhom().sendRawMessage(Utilities.PREFIX + ChatColor.RED + "Entity is no longer valid. Modification has been cancelled.");
+ if (livingEntity.isDead()) {
+ conversationContext.getForWhom().sendRawMessage(Utilities.PREFIX_STRING + ChatColor.RED + "Entity is no longer valid. Modification has been cancelled.");
return Prompt.END_OF_CONVERSATION;
}
+ StackEntity stackEntity = sm.getEntityManager().getStackEntity(livingEntity);
+ if (stackEntity == null) {
+ stackEntity = sm.getEntityManager().registerStackedEntity(livingEntity);
+ }
stackEntity.setSize(number.intValue());
- conversationContext.getForWhom().sendRawMessage(Utilities.PREFIX + ChatColor.GREEN + "Stack value has been updated.");
+ conversationContext.getForWhom().sendRawMessage(Utilities.PREFIX_STRING + ChatColor.GREEN + "Stack value has been updated.");
return Prompt.END_OF_CONVERSATION;
}
@@ -131,18 +181,15 @@ public String getPromptText(@NotNull ConversationContext conversationContext) {
@Nullable
@Override
protected String getFailedValidationText(@NotNull ConversationContext context, @NotNull String invalidInput) {
- return Utilities.PREFIX + ChatColor.RED + "Invalid input. Accepted sizes: 1-" + stackEntity.getMaxSize();
+ return Utilities.PREFIX_STRING + ChatColor.RED + "Invalid input. Accepted sizes: 1-" + maxSize;
}
@Override
protected boolean isNumberValid(@NotNull ConversationContext context, @NotNull Number input) {
- if (input.intValue() > stackEntity.getMaxSize()) {
- return false;
- }
- if (input.intValue() < 1) {
+ if (input.intValue() > maxSize) {
return false;
}
- return true;
+ return input.intValue() > 0;
}
}
@@ -153,7 +200,7 @@ public void conversationAbandoned(@NotNull ConversationAbandonedEvent conversati
if (conversationAbandonedEvent.gracefulExit()) {
return;
}
- conversationAbandonedEvent.getContext().getForWhom().sendRawMessage(Utilities.PREFIX + ChatColor.RED + "Stack modification has timed out.");
+ conversationAbandonedEvent.getContext().getForWhom().sendRawMessage(Utilities.PREFIX_STRING + ChatColor.RED + "Stack modification has timed out.");
}
}
diff --git a/src/main/java/uk/antiperson/stackmob/utils/Updater.java b/src/main/java/uk/antiperson/stackmob/utils/Updater.java
index a9201cc5..1b41796f 100644
--- a/src/main/java/uk/antiperson/stackmob/utils/Updater.java
+++ b/src/main/java/uk/antiperson/stackmob/utils/Updater.java
@@ -1,5 +1,8 @@
package uk.antiperson.stackmob.utils;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
import org.bukkit.plugin.Plugin;
import java.io.BufferedReader;
@@ -11,16 +14,16 @@
public class Updater {
- private final int resourceId;
+ private final String resourceId;
private final Plugin sm;
- public Updater(Plugin sm, int resourceId) {
+ public Updater(Plugin sm, String resourceId) {
this.sm = sm;
this.resourceId = resourceId;
}
public CompletableFuture checkUpdate() {
return CompletableFuture.supplyAsync(() -> {
- String latestVersion = getLatestVersion();
+ String latestVersion = getLatestVersion().get("version_number").getAsString();;
if (latestVersion == null) {
return new UpdateResult(VersionResult.ERROR);
}
@@ -33,12 +36,18 @@ public CompletableFuture checkUpdate() {
});
}
- private String getLatestVersion(){
+ private JsonObject getLatestVersion(){
try{
- URL updateUrl = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + resourceId);
+ URL updateUrl = new URL("https://api.modrinth.com/v2/project/" + resourceId + "/version");
HttpURLConnection connect = (HttpURLConnection) updateUrl.openConnection();
connect.setRequestMethod("GET");
- return new BufferedReader(new InputStreamReader(connect.getInputStream())).readLine();
+ connect.setRequestProperty("Accept", "application/json");
+ connect.setRequestProperty("Content-Type", "application/json");
+ connect.setRequestProperty("User-Agent", sm.getName());
+ BufferedReader reader = new BufferedReader(new InputStreamReader(connect.getInputStream()));
+ JsonArray object = JsonParser.parseReader(reader).getAsJsonArray();
+ reader.close();
+ return object.get(0).getAsJsonObject();
}catch (Exception e){
e.printStackTrace();
return null;
@@ -46,9 +55,13 @@ private String getLatestVersion(){
}
public CompletableFuture downloadUpdate() {
- File currentFile = new File(sm.getClass().getProtectionDomain().getCodeSource().getLocation().getPath());
- File updateFile = new File(sm.getServer().getUpdateFolderFile(), currentFile.getName());
- return Utilities.downloadFile(updateFile, "http://aqua.api.spiget.org/v2/resources/" + resourceId + "/download");
+ return CompletableFuture.supplyAsync(() -> {
+ File currentFile = new File(sm.getClass().getProtectionDomain().getCodeSource().getLocation().getPath());
+ File updateFile = new File(sm.getServer().getUpdateFolderFile(), currentFile.getName());
+ JsonObject latestVersion = getLatestVersion();
+ String updateUrl = latestVersion.getAsJsonArray("files").get(0).getAsJsonObject().get("url").getAsString();
+ return Utilities.downloadFile(updateFile, updateUrl);
+ });
}
public enum VersionResult {
diff --git a/src/main/java/uk/antiperson/stackmob/utils/Utilities.java b/src/main/java/uk/antiperson/stackmob/utils/Utilities.java
index e6f7b4fc..f6a14e11 100644
--- a/src/main/java/uk/antiperson/stackmob/utils/Utilities.java
+++ b/src/main/java/uk/antiperson/stackmob/utils/Utilities.java
@@ -1,6 +1,15 @@
package uk.antiperson.stackmob.utils;
-import org.bukkit.ChatColor;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextColor;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import org.bukkit.Bukkit;
+import org.bukkit.GameMode;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.EquipmentSlot;
+import org.bukkit.inventory.ItemStack;
import java.io.File;
import java.io.IOException;
@@ -8,44 +17,201 @@
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.CompletableFuture;
public class Utilities {
- public static final String PREFIX = ChatColor.DARK_GREEN + "StackMob " + ChatColor.GRAY + ">> " + ChatColor.RESET;
-
+ public static final boolean IS_FOLIA;
+ static {
+ boolean f = false;
+ try {
+ Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
+ f = true;
+ } catch (ClassNotFoundException ignored) {}
+ IS_FOLIA = f;
+ }
+ public static final Component PREFIX = Component.text("StackMob ").color(TextColor.color(0, 206, 209)).append(Component.text(">> ").color(NamedTextColor.GRAY)).compact();
+ public static final String PREFIX_STRING = LegacyComponentSerializer.legacySection().serialize(PREFIX);
public static final String SLIME_METADATA = "deathcount";
-
+ public static final String NO_LEASH_METADATA = "stop-leash";
public static final String DISCORD = "https://discord.gg/fz9xzuB";
public static final String GITHUB = "https://github.com/Nathat23/StackMob-5";
public static final String GITHUB_DEFAULT_CONFIG = GITHUB + "/tree/master/src/main/resources";
+ private static final boolean usingPaper;
+ public static final List DROWNED_MATERIALS = Arrays.asList(Material.NAUTILUS_SHELL, Material.TRIDENT);
+ public static final List HAND_SLOTS = Arrays.asList(EquipmentSlot.HAND, EquipmentSlot.OFF_HAND);
+ private static MinecraftVersion minecraftVersion;
+ private static final LegacyComponentSerializer legacyComponentSerializer = LegacyComponentSerializer.builder().character('&').hexColors().hexCharacter('#').build();
- public static CompletableFuture downloadFile(File filePath, String url) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- URL webPath = new URL(url);
- try (InputStream in = webPath.openStream()) {
- Files.createDirectories(filePath.getParentFile().toPath());
- Files.copy(in, filePath.toPath(), StandardCopyOption.REPLACE_EXISTING);
- }
- return DownloadResult.SUCCESSFUL;
- } catch (IOException e) {
- e.printStackTrace();
- return DownloadResult.ERROR;
+ static {
+ boolean usingPaper1;
+ try {
+ Bukkit.class.getMethod("spigot");
+ Bukkit.spigot().getClass().getMethod("getPaperConfig");
+ usingPaper1 = true;
+ } catch (NoSuchMethodException e) {
+ usingPaper1 = false;
+ }
+ usingPaper = usingPaper1;
+ }
+
+ public static Component createComponent(String toTranslate) {
+ return legacyComponentSerializer.deserialize(toTranslate);
+ }
+
+ public static List split(int dividend, int divisor) {
+ int fullAmount = dividend / divisor;
+ int remainder = dividend % divisor;
+ List numbers = new ArrayList<>(fullAmount + 1);
+ for (int i = 0; i < fullAmount; i++) {
+ numbers.add(divisor);
+ }
+ if (remainder > 0) {
+ numbers.add(remainder);
+ }
+ return numbers;
+ }
+
+ public static DownloadResult downloadFile(File filePath, String url) {
+ try {
+ URL webPath = new URL(url);
+ try (InputStream in = webPath.openStream()) {
+ Files.createDirectories(filePath.getParentFile().toPath());
+ Files.copy(in, filePath.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
- });
+ return DownloadResult.SUCCESSFUL;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return DownloadResult.ERROR;
+ }
}
public static boolean isPaper() {
- return Package.getPackage("com.destroystokyo.paper") != null;
+ return usingPaper;
}
- public static boolean isNewBukkit() {
- return Package.getPackage("net.minecraft.server.v1_16_R1") != null;
+ public static MinecraftVersion getMinecraftVersion() {
+ if (minecraftVersion == null) {
+ minecraftVersion = MinecraftVersion.V1_16;
+ String packageName = Bukkit.getServer().getBukkitVersion();
+ String ending = packageName.substring(0, packageName.indexOf('-'));
+ for (MinecraftVersion version: MinecraftVersion.values()) {
+ if (ending.contains(version.getInternalName())){
+ minecraftVersion = version;
+ }
+ }
+ }
+ return minecraftVersion;
+ }
+
+ public static boolean isVersionAtLeast(MinecraftVersion version) {
+ return getMinecraftVersion().ordinal() >= version.ordinal();
+ }
+
+ public static boolean isDye(ItemStack material) {
+ return material.getType().toString().endsWith("_DYE");
+ }
+
+ public static void removeHandItem(Player player, int itemAmount) {
+ if (player.getGameMode() == GameMode.CREATIVE) {
+ return;
+ }
+ if (itemAmount == player.getInventory().getItemInMainHand().getAmount()) {
+ player.getInventory().setItemInMainHand(null);
+ return;
+ }
+ ItemStack is = player.getInventory().getItemInMainHand();
+ is.setAmount(is.getAmount() - itemAmount);
+ player.getInventory().setItemInMainHand(is);
}
public enum DownloadResult {
SUCCESSFUL,
ERROR
}
+
+ public static String filter(String string) {
+ return string.replaceAll("[^A-Za-z\\d]", " ");
+ }
+
+ public static double toDouble(final String str) {
+ return toDouble(str, 0.0d);
+ }
+
+ public static double toDouble(final String str, final double defaultValue) {
+ if (str == null) {
+ return defaultValue;
+ }
+ try {
+ return Double.parseDouble(str);
+ } catch (final NumberFormatException nfe) {
+ return defaultValue;
+ }
+ }
+
+ public static int toInt(final String str) {
+ return toInt(str, 0);
+ }
+
+ public static int toInt(final String str, final int defaultValue) {
+ if (str == null) {
+ return defaultValue;
+ }
+ try {
+ return Integer.parseInt(str);
+ } catch (final NumberFormatException nfe) {
+ return defaultValue;
+ }
+ }
+
+ public static String[] removeFirst(String[] array) {
+ String[] newArray = new String[array.length - 1];
+ System.arraycopy(array, 1, newArray, 0, array.length - 1);
+ return newArray;
+ }
+
+ public static String capitalizeString(String string) {
+ String work = string.toLowerCase();
+ char[] chars = work.toCharArray();
+ boolean shouldCapitalize = true;
+ for (int i = 0; i < chars.length; i++) {
+ if (shouldCapitalize) {
+ chars[i] = Character.toTitleCase(chars[i]);
+ shouldCapitalize = false;
+ continue;
+ }
+ if (Character.isSpaceChar(chars[i])) {
+ shouldCapitalize = true;
+ }
+ }
+ return new String(chars);
+ }
+
+ public static Material getScuteMaterial() {
+ return isVersionAtLeast(MinecraftVersion.V1_20_6) ? Material.getMaterial("TURTLE_SCUTE") : Material.getMaterial("SCUTE");
+ }
+
+ public enum MinecraftVersion {
+ V1_16("1.16"),
+ V1_17("1.17"),
+ V1_18("1.18"),
+ V1_18_2("1.18.2"),
+ V1_19_4("1.19.4"),
+ V1_20_4("1.20.4"),
+ V1_20_6("1.20.6"),
+ V1_21("1.21");
+
+ final String internalName;
+
+ MinecraftVersion(String internalName) {
+ this.internalName = internalName;
+ }
+
+ public String getInternalName() {
+ return internalName;
+ }
+ }
}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index a66a7c5e..c827d2be 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -1,11 +1,17 @@
-# StackMob 5.0 Configuration file - by antiPerson and contributors.
+# StackMob ${project.version} Configuration file - by antiPerson and contributors.
+# If the comments have been removed, you can find a version with comments at https://github.com/Nathat23/StackMob-5/tree/master/src/main/resources
#
# Most options can be customised for specific entity types, for details about this, see the end of this file.
# Asterisk meaning:
# (*) This option cannot be overridden in the 'custom' section at the end of this file.
# (**) The following option requires the use of Paper (https://papermc.io) because the API needed is not in Spigot.
-#
-# If the comments have been removed, you can find a version with comments at https://github.com/Nathat23/StackMob-5/tree/master/src/main/resources
+# (***) You can use group names for entities. These are: HOSTILE, ANIMALS, WATER, RAIDER, BOSS
+# Spigot JavaDoc Reference:
+# (1) https://hub.spigotmc.org/javadocs/spigot/org/bukkit/event/entity/EntityDamageEvent.DamageCause.html
+# (2) https://hub.spigotmc.org/javadocs/spigot/org/bukkit/event/entity/CreatureSpawnEvent.SpawnReason.html
+# (3) https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/EntityType.html
+# (4) https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html
+# (5) https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/event/entity/EntityKnockbackEvent.KnockbackCause.html
stack:
# The maximum size that a stack can have.
@@ -19,41 +25,73 @@ stack:
threshold:
enabled: true
amount: 5
+ # Only find matches for this mob if it has moved from the last time nearby mobs were checked. (*)
+ # This may have unintended effects, so you should do some testing when modifying this section.
+ check-location:
+ enabled: true
+ # The distance threshold for the check
+ distance: 1.0
+ # Even if the entity hasn't moved, after (x) attempts then attempt a merge.
+ timeout: 20
+ # Stack entities when they spawn. (*)
+ on-spawn: false
+ # Only stack when two stacks can directly see each other
+ line-of-sight: true
+ # What should be done if a nametagged entity is removed when stacking?
+ # DROP - drop a nametag
+ # JOIN - make it so that the value of the nametag is added to the remaining entity
+ # NOTE: this will only work if display-name.mode is NEARBY amd display-name.nearby.use-armorstand is enabled!
+ # IGNORE - do nothing
+ # Also see events.nametag where you can define what happens when stacks are nametagged
+ nametag-mode: JOIN
# Names of worlds where there should be no stacking
worlds-blacklist: []
worlds-blacklist-invert: false
-# Types of entity which should not stack
-types-blacklist: [VILLAGER]
+# Types of entity which should not stack (3) (***)
+types-blacklist: [VILLAGER, WATER, RAIDER]
types-blacklist-invert: false
-# If the entity's spawns reason is listed below, do not stack.
-reason-blacklist: []
+# If the entity's spawn reason is listed below, do not stack. (2)
+reason-blacklist:
+ - SHOULDER_ENTITY
+ - NATURAL
reason-blacklist-invert: false
display-name:
# The formatting of the name tag which is shown when the stack size is above the threshold.
+ # Both legacy and RGB hex color codes are supported, these need to be prefixed by the '&' symbol.
# Placeholders:
# %type% - the entity's type
# %size% - the size of the stack
- format: '&a%type% &d(%size%)'
+ format: 'B2AA%type% (%size%)'
# Whether the display tag can only be seen when the player targets the entity.
# Mode:
# ALWAYS - The tag will always be visible as long as the entity is loaded.
# HOVER - The tag will only be visible when the player hovers their crosshair on the entity.
- # NEARBY - The tag will only be visible when the player is in range of the entity. (more resource intensive, requires ProtocolLib on 1.15 servers)
+ # NEARBY - The tag will only be visible when the player is in range of the entity. (more resource intensive)
visibility: NEARBY
# Options for when 'NEARBY' is used above. (*)
nearby:
# The range from which the tag should be visible.
- # Format: [(x cord),(y cord),(z cord)]
- range: [12,6,12]
+ radius: 12
# How often (in ticks) the tag visibility status of an entity should be updated. (20 ticks = 1 second)
- interval: 20
+ interval: 50
+ # Perform a ray trace, meaning that the tag is only visible if there are no occluding blocks.
+ ray-trace: true
+ # Use an armor stand (using packets) to display the tag, rather than renaming the entity.
+ # This allows the entity to be renamed using name tags
+ armorstand:
+ enabled: true
+ # Whether the armor stand should be spawned at a fixed offset from the head location.
+ # This is useful when other plugins use packets for entity name tags. If the value is 0.0 then this is disabled.
+ offset: 0.0
# Don't show the tag if the stack size of this entity if it is equal to or below the value specified.
threshold: 1
# Whether entity specific traits (eg. profession, colour) should be segregated (*)
traits:
+ allay-owner: true
+ frog-variant: true
sheep-color: true
sheep-sheared: true
slime-size: true
@@ -71,86 +109,146 @@ traits:
bee-stung: true
zoglin-baby: true
piglin-baby: true
+ leashed: true
+ villager-profession: true
+ potion-effect: true
+ # Prevent pregnant turtles from stacking. (**)
+ has-egg: true
# Prevent stacked mobs from targeting players.
# Similar to no-ai, but allows for movement of entities.
disable-targeting:
enabled: false
- # If the entity's type is listed below, do not disable targeting.
+ # If the entity's type is listed below, do not disable targeting. (3) (***)
type-blacklist: []
type-blacklist-invert: false
- # If the entity's spawns reason is listed below, do not disable targeting (**)
+ # If the entity's spawns reason is listed below, do not disable targeting (**) (2)
reason-blacklist: []
reason-blacklist-invert: false
+# Prevent stacked mobs from taking knockback.
+# Stacked mobs will still be able to move, but will stay still if knockback occurs
+disable-knockback:
+ enabled: false
+ # If the entity's type is listed below, do not disable knockback. (3) (***)
+ type-blacklist: []
+ type-blacklist-invert: false
+ # If the entity's spawns reason is listed below, do not disable knockback (**) (2)
+ reason-blacklist: []
+ reason-blacklist-invert: false
+ # If the cause of knockback is listed below, do not disable knockback (**) (5)
+ cause-blacklist: []
+ cause-blacklist-invert: false
+
# What should be done when these entity actions occur. (*)
events:
+ # When a nametag is used to rename a mob (see nametag)
+ nametag:
+ enabled: true
+ # SLICE - slice off an entity so that a single entity is named and becomes unstackable
+ # PREVENT - prevent nametags from working with stacked mobs
+ mode: SLICE
+ # When an entity picks up armor or other equipment
+ equip:
+ enabled: true
+ # IGNORE - do not take picked up equipment into consideration when stacking
+ # DROP_ITEMS - drop picked up items when about to stack
+ # PREVENT_STACK - do not stack this entity.
+ mode: IGNORE
# When an entity is feed its food.
breed:
enabled: true
# MULTIPLY - spawn as many baby entities possible providing there is enough food fed.
# SPLIT - slice off an entity to that it can be bred normally.
mode: MULTIPLY
+ # Limit the amount of baby entities spawned when using MULTIPLY mode. -1 = disabled.
+ limit: -1
# When an entity is dyed.
dye:
enabled: true
# MULTIPLY - dye as many entities possible providing there is enough dye.
# SPLIT - slice off an entity to that it can be dyed normally.
mode: MULTIPLY
+ # Limit the amount of sheep dyed when using MULTIPLY mode. -1 = disabled.
+ limit: -1
# When an entity is sheared.
shear:
enabled: true
# MULTIPLY - shear as many entities possible providing the shears have enough durability.
# SPLIT - slice off an entity to that it can be sheared normally.
mode: MULTIPLY
+ # Limit the amount of sheep sheared when using MULTIPLY mode. -1 = disabled.
+ limit: -1
+ # When an entity explodes.
+ explosion:
+ enabled: true
+ # MULTIPLY - increase the damage done by the explosion for each entity in the stack.
+ # SPLIT - slice off an entity so that only one entity explodes.
+ mode: MULTIPLY
+ # Limit the amount of damage dealt when using MULTIPLY mode. -1 = disabled.
+ limit: -1
multiply:
# Chicken eggs and turtle scutes.
drops: true
- explosion: true
slime-split: true
divide:
- nametag: true
tame: true
- remove-stack-data:
- # Should we remove the stack data of an entity that was unstacked as a result of nametag division/split?
- # Setting this to false will cause entities that were nametagged to potentially become stacked again.
- nametag: true
+ # Only bees currently.
+ enter-block: true
+ # Only fish currently.
+ bucket-fill: true
# How entities should die
death:
- # Death options:
+ # Rather than spawn a new entity in place of the killed entity, just decrease the stack size instead. (**)
+ skip-animation: false
+ # Death amount options:
# SINGLE - Only one entity dies.
# ALL - Every entity dies.
# STEP - A random amount dies.
# STEP_DAMAGE - An amount that depends on the damage done dies.
#
+ # Each death option has its own blacklist for entity traits:
+ # reason-blacklist - the last damage cause of the entity, which results in is death. (1)
+ # spawn-reason-blacklist - the spawn reason of the entity. (**) (2)
+ # type-blacklist - entity type. (***) (3)
+ #
# The priority 1 is highest and 4 is lowest.
- # If the killed entity is blacklisted, either by type or death reasons, by the highest priority method,
- # then it will attempt to use the next highest priority death method.
+ # If the killed entity is blacklisted, either by type, spawn reason or death reasons (the last damage cause),
+ # by the highest priority method, then we will attempt to use the next highest priority death method.
SINGLE:
- priority: 4
+ priority: 2
reason-blacklist: []
reason-blacklist-invert: false
+ spawn-reason-blacklist: []
+ spawn-reason-blacklist-invert: false
type-blacklist: []
type-blacklist-invert: false
ALL:
priority: 3
reason-blacklist: []
reason-blacklist-invert: false
+ spawn-reason-blacklist: [ ]
+ spawn-reason-blacklist-invert: false
type-blacklist: []
type-blacklist-invert: false
STEP:
- priority: 2
+ priority: 1
reason-blacklist: []
reason-blacklist-invert: false
+ spawn-reason-blacklist: [ ]
+ spawn-reason-blacklist-invert: false
type-blacklist: []
type-blacklist-invert: false
max-step: 5
min-step: 1
+ # This mode is kind of buggy, especially when custom mob plugins are used.
STEP_DAMAGE:
- priority: 1
+ priority: 4
reason-blacklist: []
reason-blacklist-invert: false
+ spawn-reason-blacklist: [ ]
+ spawn-reason-blacklist-invert: false
type-blacklist: []
type-blacklist-invert: false
@@ -158,16 +256,16 @@ death:
drops:
enabled: true
use-loot-tables: true
- # If each mob should only drop one of the items in the list.
+ # If each mob should only drop one of the items in the list. (4)
one-per-stack: []
one-per-stack-invert: false
- # Items that should not be dropped.
+ # Items that should not be dropped. (4)
item-blacklist: []
item-blacklist-invert: false
- # Death reasons that should not mean that drops are multiplied.
+ # Death reasons that should not mean that drops are multiplied. (1)
reason-blacklist: []
reason-blacklist-invert: false
- # Types of entity for which drops should not be dropped for.
+ # Types of entity for which drops should not be dropped for. (3) (***)
type-blacklist: []
type-blacklist-invert: false
@@ -177,7 +275,7 @@ experience:
# The bounds that should be used for the random multiplier.
multiplier-min: 0.5
multiplier-max: 0.8
- # Types of entity for which exp should not be dropped for.
+ # Types of entity for which exp should not be dropped for. (3)
type-blacklist: []
type-blacklist-invert: false
@@ -190,15 +288,15 @@ wait-to-stack:
enabled: true
# For (x) times the stack task fires after spawn, don't stack this entity.
wait-time: 5
- # Entity types that this should work for.
+ # Entity types that this should work for. (3) (***)
types-whitelist:
- ZOMBIE
- SKELETON
- - PIG_ZOMBIE
+ - ZOMBIFIED_PIGLIN
- CREEPER
- ENDERMAN
types-whitelist-invert: false
- # Spawn reasons that this should work for.
+ # Spawn reasons that this should work for. (2)
reasons-whitelist:
- SPAWNER
reasons-whitelist-invert: false
@@ -208,17 +306,31 @@ hooks:
# Allows the custom 'entity-stacking' flag to be used in worldguard regions.
worldguard: true
mythicmobs:
- enabled: true
- blacklist: []
- blacklist-invert: false
+ stack: false
+ # Blacklist of MythicMob types (uses the internal name)
+ # This blacklist does not take effect if the prevent-stack option is enabled below.
+ stack-blacklist: [ ]
+ stack-blacklist-invert: false
+ # When this option is enabled, MythicMobs will not stack.
+ prevent-stack: true
# Prevent citizens npcs from stacking.
citizens: true
# Prevent stacked entities from giving mcmmo experience.
mcmmo: true
- # Prevent stacked entities from giving job payments.
- jobs: true
+ # Options relating to the Jobs Reborn plugin
+ jobs:
+ enabled: true
+ # Modes:
+ # PREVENT - Do not give Jobs rewards for stacked mobs.
+ # IGNORE - Make it so StackMob doesn't touch Jobs.
+ # MULTIPLY - Multiply rewards for stacked mobs.
+ mode: PREVENT
# Allows the use of ProtocolLib, which is used when the server version is not the native one of the plugin.
protocollib: true
+ # Make it so that ClearLagg removes stacked entities.
+ clearlagg: false
+ # Make it so that MyPet pets do not stack.
+ mypet: true
## Some options can be modified for specific entity types. An example is shown below. Remove comments to see this in action.
## Options and sections with a (*) in the comment preceding cannot be overridden.
@@ -234,5 +346,3 @@ hooks:
# # The cloned custom options can still be overridden.
# stack:
# max-size: 5
-
-
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index cd04d105..4a5a519c 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -1,14 +1,15 @@
name: StackMob
main: uk.antiperson.stackmob.StackMob
version: ${project.version}
-api-version: 1.15
-softdepend: [WorldGuard, MythicMobs, ProtocolLib]
+api-version: 1.16
+folia-supported: true
+softdepend: [WorldGuard, MythicMobs, ClearLag, MyPet, mcMMO, Jobs, Citizens]
author: antiPerson
-website: https://www.spigotmc.org/resources/stackmob-enhance-your-servers-performance-without-the-sacrifice.29999/
-description: A plugin that aims to improve performance by 'stacking' entities together, with attempts to perserve game mechanics.
+website: https://modrinth.com/plugin/stackmob
+description: A plugin that aims to improve performance by 'stacking' entities together, with attempts to preserve game mechanics.
commands:
stackmob:
aliases: [sm]
permission: stackmob.admin
- description: The main command of stackmob, which allows for maniplulation of stacks.
+ description: The main command of stackmob, which allows for manipulation of stacks.
permission-message: No permission!
\ No newline at end of file