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.stackmob StackMob - 5.2.0 + 5.10.3 src/main/java @@ -33,27 +33,31 @@ 1.3 + + UTF-8 + org.apache.maven.plugins maven-compiler-plugin - 3.7.0 + 3.10.1 - 1.8 - 1.8 + 17 + 17 org.apache.maven.plugins maven-shade-plugin - 3.1.0 + 3.5.3 org.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.io https://jitpack.io + + lumine-repo + https://mvn.lumine.io/repository/maven-public/ + - com.destroystokyo.paper + io.papermc.paper paper-api - 1.16.1-R0.1-SNAPSHOT + 1.21.1-R0.1-SNAPSHOT provided - org.spigotmc - spigot - 1.16.1-R0.1-SNAPSHOT + dev.folia + folia-api + 1.20.6-R0.1-SNAPSHOT provided com.sk89q.worldguard - worldguard-legacy - 7.0.0-SNAPSHOT + worldguard-bukkit + 7.1.0-SNAPSHOT provided com.sk89q.worldedit worldedit-bukkit - 7.1.0-SNAPSHOT + 7.2.16-SNAPSHOT provided org.bstats bstats-bukkit - 1.5 + 3.0.2 compile - 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 + provided com.github.Zrips Jobs - LATEST + v5.2.2.3 provided - 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 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 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 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[] classes; + @SafeVarargs + EntityGrouping(Class... classes) { + this.classes = classes; + } + + public boolean isEntityMemberOf(Class entity) { + if (entity == null) { + return false; + } + for (Class 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 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 typeArgument = (Class) 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 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 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 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 hookClass) throws IllegalAccessE * @throws InstantiationException if the class is abstract */ private Hook createInstance(Class 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