commit e3422f22c6f66f326ca74065b8efed117a30cdce Author: 3328429240 <3328429240@qq.com> Date: Thu Jul 10 18:10:49 2025 +0800 Initial commit diff --git a/.github/znpc.png b/.github/znpc.png new file mode 100644 index 0000000..06a8686 Binary files /dev/null and b/.github/znpc.png differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb5a603 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +/.idea/* +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +/plugin/run/ +/.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /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 new file mode 100644 index 0000000..49b1a42 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# ZNPCsPlus [![](https://img.shields.io/discord/1099449144948555957?label=Discord&logo=Discord&style=plastic)](https://discord.gg/MAZz6XpPcg) [![](https://img.shields.io/jenkins/build?jobUrl=https%3A%2F%2Fci.pyr.lol%2Fjob%2FZNPCsPlus%2F&style=plastic&logo=jenkins)](https://ci.pyr.lol/job/ZNPCsPlus/) +[![](https://img.shields.io/bstats/players/18244?style=plastic&label=bStats%20Players)]((https://bstats.org/plugin/bukkit/ZNPCsPlus/18244/)) [![](https://img.shields.io/bstats/servers/18244?style=plastic&label=bStats%20Servers)]((https://bstats.org/plugin/bukkit/ZNPCsPlus/18244/)) [![](https://img.shields.io/spiget/downloads/109380?style=plastic&label=Spigot%20Downloads)]((https://www.spigotmc.org/resources/znpcsplus.109380/)) + +[ZNPCsPlus](https://www.spigotmc.org/resources/znpcsplus.109380/) is a Spigot plugin that is used to create fake entities +that players can interact with to perform actions like switching servers on a network or executing commands. + +This plugin is a remake of a plugin called ZNPCs, we originally started because the maintainer of ZNPCs decided to announce that he was +[dropping support for the plugin](https://github.com/Pyrbu/ZNPCsPlus/blob/2.X/.github/znpc.png?raw=true). + +Looking for up-to-date builds of the plugin? Check out our [Jenkins](https://ci.pyr.lol/job/ZNPCsPlus/) + +## Why is it so good? +- 100% Packet Based - Nothing is ran on the main thread +- Performance & stability oriented code +- Support for all versions from 1.8 to 1.20.4 +- Support for multiple different storage options +- Intuitive command system + +### Requirements, Extensions & Supported Software +Requirements: +- Java 8+ +- Minecraft 1.8 - 1.21 + +Supported Softwares: +- Spigot ([Website](https://www.spigotmc.org/)) +- Paper ([Github](https://github.com/PaperMC/Paper)) ([Website](https://papermc.io/software/paper)) +- Folia ([Github](https://github.com/PaperMC/Folia)) ([Website](https://papermc.io/software/folia)) +- ArcLight ([Github](https://github.com/IzzelAliz/Arclight)) + +Optional Dependencies/Extensions: +- PlaceholderAPI + +## Found a bug? +Open an issue in the GitHub [issue tracker](https://github.com/Pyrbu/ZNPCsPlus/issues) or join our [support discord](https://discord.gg/MAZz6XpPcg) + +## BStats +[![](https://bstats.org/signatures/bukkit/znpcsplus.svg)](https://bstats.org/plugin/bukkit/ZNPCsPlus/18244/) + +#### Like what you see? Want the project to continue improving? Consider starring the repository & leaving a positive review on [Spigot](https://www.spigotmc.org/resources/znpcsplus.109380/)! + +## Credits +- [PacketEvents 2.0](https://github.com/retrooper/packetevents) - Packet library +- [Minecraft Wiki Protocol (formally wiki.vg)](https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Main_Page) - Minecraft protocol documentation +- [gson](https://github.com/google/gson) - JSON parsing library made by Google +- [Mineskin.org](https://mineskin.org/) - Website for raw skin file uploads +- [adventure](https://docs.advntr.dev/) - Minecraft text api +- [DazzleConf](https://github.com/A248/DazzleConf) - Configuration library +- [Director](https://github.com/Pyrbu/Director) - Command library +- [PlaceholderAPI](https://github.com/PlaceholderAPI/PlaceholderAPI) - Universal string placeholder library diff --git a/api/build.gradle b/api/build.gradle new file mode 100644 index 0000000..349cf17 --- /dev/null +++ b/api/build.gradle @@ -0,0 +1,35 @@ +plugins { + id "java" + id "maven-publish" +} + +java { + withSourcesJar() + withJavadocJar() +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifactId = "znpcsplus-api" + + pom { + name.set("znpcsplus-api") + description.set("The API for the ZNPCsPlus plugin") + url.set("https://github.com/Pyrbu/ZNPCsPlus") + } + } + } + repositories { + maven { + Map systemProperties = System.getenv() + credentials { + if (systemProperties.containsKey("DIST_USERNAME")) username systemProperties.get("DIST_USERNAME") + if (systemProperties.containsKey("DIST_PASSWORD")) password systemProperties.get("DIST_PASSWORD") + } + // If the BUILD_ID enviroment variable is present that means its a Jenkins build & that it should go into the snapshots repo + url = systemProperties.containsKey("BUILD_ID") ? uri("https://repo.pyr.lol/snapshots/") : uri("https://repo.pyr.lol/releases/") + } + } +} \ No newline at end of file diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/NpcApi.java b/api/src/main/java/lol/pyr/znpcsplus/api/NpcApi.java new file mode 100644 index 0000000..2faf2c8 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/NpcApi.java @@ -0,0 +1,56 @@ +package lol.pyr.znpcsplus.api; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.api.interaction.ActionFactory; +import lol.pyr.znpcsplus.api.interaction.ActionRegistry; +import lol.pyr.znpcsplus.api.npc.NpcRegistry; +import lol.pyr.znpcsplus.api.npc.NpcTypeRegistry; +import lol.pyr.znpcsplus.api.serialization.NpcSerializerRegistry; +import lol.pyr.znpcsplus.api.skin.SkinDescriptorFactory; + +/** + * Main API class for ZNPCsPlus. + */ +public interface NpcApi { + /** + * Gets the NPC registry. + * @return the NPC registry + */ + NpcRegistry getNpcRegistry(); + + /** + * Gets the NPC type registry. + * @return the NPC type registry + */ + NpcTypeRegistry getNpcTypeRegistry(); + + /** + * Gets the entity property registry. + * @return the entity property registry + */ + EntityPropertyRegistry getPropertyRegistry(); + + /** + * Gets the action registry. + * @return the action registry + */ + ActionRegistry getActionRegistry(); + + /** + * Gets the action factory. + * @return the action factory + */ + ActionFactory getActionFactory(); + + /** + * Gets the skin descriptor factory. + * @return the skin descriptor factory + */ + SkinDescriptorFactory getSkinDescriptorFactory(); + + /** + * Gets the npc serializer registry. + * @return the npc serializer registry + */ + NpcSerializerRegistry getNpcSerializerRegistry(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/NpcApiProvider.java b/api/src/main/java/lol/pyr/znpcsplus/api/NpcApiProvider.java new file mode 100644 index 0000000..b35235c --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/NpcApiProvider.java @@ -0,0 +1,50 @@ +package lol.pyr.znpcsplus.api; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.ServicePriority; + +/** + * Provider for the registered api instance + */ +public class NpcApiProvider { + private static NpcApi api = null; + + private NpcApiProvider() { + throw new UnsupportedOperationException(); + } + + /** + * Static method that returns the api instance of the plugin + * + * @return The instance of the api for the ZNPCsPlus plugin + */ + public static NpcApi get() { + if (api == null) throw new IllegalStateException( + "ZNPCsPlus plugin isn't enabled yet!\n" + + "Please add it to your plugin.yml as a depend or softdepend." + ); + return api; + } + + /** + * Internal method used to register the main instance of the plugin as the api provider + * You probably shouldn't call this method under any circumstances + * + * @param plugin Instance of the ZNPCsPlus plugin + * @param api Instance of the ZNPCsPlus api + */ + public static void register(Plugin plugin, NpcApi api) { + NpcApiProvider.api = api; + Bukkit.getServicesManager().register(NpcApi.class, api, plugin, ServicePriority.Normal); + } + + /** + * Internal method used to unregister the plugin from the provider when the plugin shuts down + * You probably shouldn't call this method under any circumstances + */ + public static void unregister() { + Bukkit.getServicesManager().unregister(api); + NpcApiProvider.api = null; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/NpcPropertyRegistryProvider.java b/api/src/main/java/lol/pyr/znpcsplus/api/NpcPropertyRegistryProvider.java new file mode 100644 index 0000000..bcfd0b5 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/NpcPropertyRegistryProvider.java @@ -0,0 +1,50 @@ +package lol.pyr.znpcsplus.api; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.ServicePriority; + +/** + * Provider for the registered entity property registry instance + */ +public class NpcPropertyRegistryProvider { + private static EntityPropertyRegistry registry = null; + + private NpcPropertyRegistryProvider() { + throw new UnsupportedOperationException(); + } + + /** + * Static method that returns the entity property registry instance of the plugin + * + * @return The instance of the entity property registry for the ZNPCsPlus plugin + */ + public static EntityPropertyRegistry get() { + if (registry == null) throw new IllegalStateException( + "ZNPCsPlus plugin isn't loaded yet!\n" + + "Please add it to your plugin.yml as a depend or softdepend." + ); + return registry; + } + + /** + * Internal method used to register the main instance of the plugin as the entity property registry provider + * You probably shouldn't call this method under any circumstances + * + * @param plugin Instance of the ZNPCsPlus plugin + * @param api Instance of the ZNPCsPlus entity property registry + */ + public static void register(Plugin plugin, EntityPropertyRegistry api) { + NpcPropertyRegistryProvider.registry = api; + Bukkit.getServicesManager().register(EntityPropertyRegistry.class, registry, plugin, ServicePriority.Normal); + } + + /** + * Internal method used to unregister the plugin from the provider when the plugin shuts down + * You probably shouldn't call this method under any circumstances + */ + public static void unregister() { + Bukkit.getServicesManager().unregister(registry); + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/entity/EntityProperty.java b/api/src/main/java/lol/pyr/znpcsplus/api/entity/EntityProperty.java new file mode 100644 index 0000000..6ad3f89 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/entity/EntityProperty.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.api.entity; + +/** + * Class that represents a unique property + * @param The type of the value of this property + */ +public interface EntityProperty { + /** + * The default value of this property, if this is provided in {@link PropertyHolder#setProperty(EntityProperty, Object)} + * as the value the property will be removed from the holder + * + * @return The default value of this property + */ + T getDefaultValue(); + + /** + * @return The name of this property + */ + String getName(); + + /** + * @return Whether this property can be modified by players using commands + */ + boolean isPlayerModifiable(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/entity/EntityPropertyRegistry.java b/api/src/main/java/lol/pyr/znpcsplus/api/entity/EntityPropertyRegistry.java new file mode 100644 index 0000000..13c094f --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/entity/EntityPropertyRegistry.java @@ -0,0 +1,55 @@ +package lol.pyr.znpcsplus.api.entity; + +import java.util.Collection; + +/** + * Class responsible for providing entity property keys + * Some property keys are only registered in certain situations for example different minecraft versions + */ +public interface EntityPropertyRegistry { + /** + * @return All of the possible property keys + */ + Collection> getAll(); + + /** + * Get a property key by it's name + * + * @param name The name of a property key + * @return The property key corresponding to the name or null if there is none + */ + EntityProperty getByName(String name); + + /** + * Get a property key by it's name and automatically cast the property to the proper type + * If you don't know the type of the property you are requesting use {@link EntityPropertyRegistry#getByName(String)} instead + * + * @param name The name of a property key + * @param type The class of the expected type of the returned property key + * @return The property key corresponding to the name + * @param The expected type of the returned property key + */ + EntityProperty getByName(String name, Class type); + + /** + * Register a dummy property that can be used to store unique information per npc
+ * Note: Properties registered this way will be player-modifiable by default + * + * @param name The name of the new property + * @param type The type of the new property + * @deprecated Use {@link #registerDummy(String, Class, boolean)} instead + */ + @Deprecated + default void registerDummy(String name, Class type) { + registerDummy(name, type, true); + } + + /** + * Register a dummy property that can be used to store unique information per npc + * + * @param name The name of the new property + * @param type The type of the new property + * @param playerModifiable Whether this property can be modified by players using commands + */ + void registerDummy(String name, Class type, boolean playerModifiable); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/entity/PropertyHolder.java b/api/src/main/java/lol/pyr/znpcsplus/api/entity/PropertyHolder.java new file mode 100644 index 0000000..5de5ea9 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/entity/PropertyHolder.java @@ -0,0 +1,61 @@ +package lol.pyr.znpcsplus.api.entity; + +import org.bukkit.inventory.ItemStack; + +import java.util.Set; + +/** + * Represents classes that have property values attatched to them + */ +public interface PropertyHolder { + /** + * Method used to get the value of a property from a property holder + * + * @param key Unique key representing a property + * @return The value associated with the provided property key and this holder + * @param The type of the property value + */ + T getProperty(EntityProperty key); + + /** + * Method used to check if a property holder has a value set for a specific property key + * + * @param key Unique key representing a property + * @return Whether this holder has a value set for the provided key + */ + boolean hasProperty(EntityProperty key); + + /** + * Method used to set a value for the provided key on this property holder + * + * @param key Unique key representing a property + * @param value The value to assign to the property key on this holder + * @param The type of the property value + */ + void setProperty(EntityProperty key, T value); + + /** + * Weird fix which is sadly required in order to not decrease performance + * when using item properties, read https://github.com/Pyrbu/ZNPCsPlus/pull/129#issuecomment-1948777764 + * + * @param key Unique key representing a property + * @param value The value to assign to the property key on this holder + */ + void setItemProperty(EntityProperty key, ItemStack value); + + /** + * Weird fix which is sadly required in order to not decrease performance + * when using item properties, read https://github.com/Pyrbu/ZNPCsPlus/pull/129#issuecomment-1948777764 + * + * @param key Unique key representing a property + * @return the {@link ItemStack} associated with the provided property key and this holder + */ + ItemStack getItemProperty(EntityProperty key); + + /** + * Method used to get a set of all of the property keys that this holder has a value for + * + * @return Set of property keys + */ + Set> getAppliedProperties(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcDespawnEvent.java b/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcDespawnEvent.java new file mode 100644 index 0000000..f9fcc01 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcDespawnEvent.java @@ -0,0 +1,32 @@ +package lol.pyr.znpcsplus.api.event; + +import lol.pyr.znpcsplus.api.event.util.CancellableNpcEvent; +import lol.pyr.znpcsplus.api.npc.NpcEntry; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; + +/** + * Event called when an NPC is despawned for a player + * Note: This event is async + */ +public class NpcDespawnEvent extends CancellableNpcEvent implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + + /** + * @param player The player involved in the event + * @param entry The NPC entry involved in the event + */ + public NpcDespawnEvent(Player player, NpcEntry entry) { + super(player, entry); + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcInteractEvent.java b/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcInteractEvent.java new file mode 100644 index 0000000..84f63b8 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcInteractEvent.java @@ -0,0 +1,45 @@ +package lol.pyr.znpcsplus.api.event; + +import lol.pyr.znpcsplus.api.event.util.CancellableNpcEvent; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.api.npc.NpcEntry; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; + +/** + * Event called when an NPC is interacted with by a player + * Note: This event is async + */ +public class NpcInteractEvent extends CancellableNpcEvent implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + + private final InteractionType clickType; + + /** + * @param player The player involved in the event + * @param entry The NPC entry involved in the event + * @param clickType The type of interaction. See {@link InteractionType} + */ + public NpcInteractEvent(Player player, NpcEntry entry, InteractionType clickType) { + super(player, entry); + this.clickType = clickType; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + /** + * Returns the type of interaction. See {@link InteractionType} + * @return The type of interaction + */ + public InteractionType getClickType() { + return clickType; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcSpawnEvent.java b/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcSpawnEvent.java new file mode 100644 index 0000000..1edeec6 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcSpawnEvent.java @@ -0,0 +1,32 @@ +package lol.pyr.znpcsplus.api.event; + +import lol.pyr.znpcsplus.api.event.util.CancellableNpcEvent; +import lol.pyr.znpcsplus.api.npc.NpcEntry; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; + +/** + * Event called when an NPC is spawned for a player + * Note: This event is async + */ +public class NpcSpawnEvent extends CancellableNpcEvent implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + + /** + * @param player The player involved in the event + * @param entry The NPC entry involved in the event + */ + public NpcSpawnEvent(Player player, NpcEntry entry) { + super(player, entry); + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/event/util/CancellableNpcEvent.java b/api/src/main/java/lol/pyr/znpcsplus/api/event/util/CancellableNpcEvent.java new file mode 100644 index 0000000..558a55e --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/event/util/CancellableNpcEvent.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.api.event.util; + +import lol.pyr.znpcsplus.api.npc.NpcEntry; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; + +/** + * Base class for all NPC events that can be cancelled + */ +public abstract class CancellableNpcEvent extends NpcEvent implements Cancellable { + private boolean cancelled = false; + + /** + * @param player The player involved in the event + * @param entry The NPC entry involved in the event + */ + public CancellableNpcEvent(Player player, NpcEntry entry) { + super(player, entry); + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + cancelled = cancel; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/event/util/NpcEvent.java b/api/src/main/java/lol/pyr/znpcsplus/api/event/util/NpcEvent.java new file mode 100644 index 0000000..cc5483f --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/event/util/NpcEvent.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.api.event.util; + +import lol.pyr.znpcsplus.api.npc.Npc; +import lol.pyr.znpcsplus.api.npc.NpcEntry; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; + +/** + * Base class for all NPC events + */ +public abstract class NpcEvent extends Event { + private final NpcEntry entry; + private final Player player; + + /** + * @param player The player involved in the event + * @param entry The NPC entry involved in the event + */ + public NpcEvent(Player player, NpcEntry entry) { + super(true); // All events are async since 99% of the plugin is async + this.entry = entry; + this.player = player; + } + + /** + * Returns the player involved in the event + * @return The player involved in the event + */ + public Player getPlayer() { + return player; + } + + /** + * Returns the NPC entry involved in the event + * @return The NPC entry involved in the event + */ + public NpcEntry getEntry() { + return entry; + } + + /** + * Returns the NPC involved in the event + * @return The NPC involved in the event + */ + public Npc getNpc() { + return entry.getNpc(); + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/hologram/Hologram.java b/api/src/main/java/lol/pyr/znpcsplus/api/hologram/Hologram.java new file mode 100644 index 0000000..b19e905 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/hologram/Hologram.java @@ -0,0 +1,56 @@ +package lol.pyr.znpcsplus.api.hologram; + +/** + * Represents a hologram + */ +public interface Hologram { + /** + * Adds a line to the hologram + * Note: to add an item line, pass "item:<item>" as the line + * @param line The line to add + */ + void addLine(String line); + + /** + * Gets a line from the hologram + * @param index The index of the line to get + * @return The line at the index + */ + String getLine(int index); + + /** + * Removes a line from the hologram + * @param index The index of the line to remove + */ + void removeLine(int index); + + /** + * Clears all lines from the hologram + */ + void clearLines(); + + /** + * Inserts a line into the hologram + * @param index The index to insert the line at + * @param line The line to insert + */ + void insertLine(int index, String line); + + /** + * Gets the number of lines in the hologram + * @return The number of lines in the hologram + */ + int lineCount(); + + /** + * Gets the refresh delay of the hologram + * @return The refresh delay of the hologram + */ + long getRefreshDelay(); + + /** + * Sets the refresh delay of the hologram + * @param delay The delay to set + */ + void setRefreshDelay(long delay); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/interaction/ActionFactory.java b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/ActionFactory.java new file mode 100644 index 0000000..b896c5f --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/ActionFactory.java @@ -0,0 +1,10 @@ +package lol.pyr.znpcsplus.api.interaction; + +@SuppressWarnings("unused") +public interface ActionFactory { + InteractionAction createConsoleCommandAction(String command, InteractionType interactionType, long cooldown, long delay); + InteractionAction createMessageAction(String message, InteractionType interactionType, long cooldown, long delay); + InteractionAction createPlayerChatAction(String message, InteractionType interactionType, long cooldown, long delay); + InteractionAction createPlayerCommandAction(String command, InteractionType interactionType, long cooldown, long delay); + InteractionAction createSwitchServerAction(String server, InteractionType interactionType, long cooldown, long delay); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/interaction/ActionRegistry.java b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/ActionRegistry.java new file mode 100644 index 0000000..6f5cedb --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/ActionRegistry.java @@ -0,0 +1,7 @@ +package lol.pyr.znpcsplus.api.interaction; + +public interface ActionRegistry { + void register(InteractionActionType type); + + void unregister(Class clazz); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionAction.java b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionAction.java new file mode 100644 index 0000000..401784a --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionAction.java @@ -0,0 +1,76 @@ +package lol.pyr.znpcsplus.api.interaction; + +import org.bukkit.entity.Player; + +import java.util.UUID; + +/** + * Base class for all NPC interactions + */ +public abstract class InteractionAction { + /** + * The unique ID of this interaction + */ + private final UUID id; + + /** + * The cooldown of this interaction in seconds + */ + private final long cooldown; + + /** + * The delay of this interaction in ticks + */ + private final long delay; + + /** + * The type of this interaction + */ + private final InteractionType interactionType; + + /** + * @param cooldown The cooldown of this interaction in seconds + * @param delay The delay of this interaction in ticks + * @param interactionType The type of this interaction + */ + protected InteractionAction(long cooldown, long delay, InteractionType interactionType) { + this.interactionType = interactionType; + this.id = UUID.randomUUID(); + this.cooldown = cooldown; + this.delay = delay; + } + + /** + * @return The unique ID of this interaction + */ + public UUID getUuid() { + return id; + } + + /** + * @return The cooldown of this interaction in seconds + */ + public long getCooldown() { + return cooldown; + } + + /** + * @return The delay of this interaction in ticks + */ + public long getDelay() { + return delay; + } + + /** + * @return The type of this interaction + */ + public InteractionType getInteractionType() { + return interactionType; + } + + /** + * Runs this interaction + * @param player The player that triggered this interaction + */ + public abstract void run(Player player); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionActionType.java b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionActionType.java new file mode 100644 index 0000000..d8289fa --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionActionType.java @@ -0,0 +1,7 @@ +package lol.pyr.znpcsplus.api.interaction; + +public interface InteractionActionType { + String serialize(T obj); + T deserialize(String str); + Class getActionClass(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionType.java b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionType.java new file mode 100644 index 0000000..27121ef --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionType.java @@ -0,0 +1,13 @@ +package lol.pyr.znpcsplus.api.interaction; + +/** + * The type of interaction + * ANY_CLICK: Any click type + * LEFT_CLICK: Only left clicks + * RIGHT_CLICK: Only right clicks + */ +public enum InteractionType { + ANY_CLICK, + LEFT_CLICK, + RIGHT_CLICK +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/npc/Npc.java b/api/src/main/java/lol/pyr/znpcsplus/api/npc/Npc.java new file mode 100644 index 0000000..79872ba --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/npc/Npc.java @@ -0,0 +1,212 @@ +package lol.pyr.znpcsplus.api.npc; + +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.api.hologram.Hologram; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.util.NpcLocation; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Base class for all NPCs + */ +public interface Npc extends PropertyHolder { + /** + * Sets the npc type of this NPC + * @param type The {@link NpcType} to set + */ + void setType(NpcType type); + + /** + * @return The {@link NpcType} of this NPC + */ + NpcType getType(); + + /** + * Gets the location of this NPC + * @return The {@link NpcLocation} of this NPC + */ + NpcLocation getLocation(); + + /** + * Sets the location of this NPC + * @param location The {@link NpcLocation} to set + */ + void setLocation(NpcLocation location); + + /** + * Sets the world of this NPC + * @param world The bukkit world to set + */ + void setWorld(World world); + + /** + * Sets the world of this NPC + * @param name The name world to set + */ + void setWorld(String name); + + /** + * Gets the hologram of this NPC + * @return The {@link Hologram} of this NPC + */ + Hologram getHologram(); + + /** + * Sets if the npc is enabled or not, i.e. if it should be visible to players + * @param enabled If the npc should be enabled + */ + void setEnabled(boolean enabled); + + /** + * Gets if the npc is enabled or not, i.e. if it should be visible to players + * @return If the npc is enabled or not + */ + boolean isEnabled(); + + /** + * Gets the unique ID of this NPC + * @return The unique ID of this NPC + */ + UUID getUuid(); + + /** + * Gets the {@link World} this NPC is in + * Note: can be null if the world is unloaded or does not exist + * @return The {@link World} this NPC is in + */ + World getWorld(); + + /** + * Gets the name of the world this NPC is in + * Unlike {@link Npc#getWorld()} this will never be null + * @return The name of the world this NPC is in + */ + String getWorldName(); + + /** + * Gets the list of actions for this NPC + * @return The {@link List} of {@link InteractionAction}s for this NPC + */ + List getActions(); + + /** + * Removes an action from this NPC + * @param index The index of the action to remove + */ + void removeAction(int index); + + /** + * Adds an action to this NPC + * @param action The {@link InteractionAction} to add + */ + void addAction(InteractionAction action); + + /** + * Edits an action for this NPC + * @param index The index of the action to edit + * @param action The {@link InteractionAction} to set + */ + void editAction(int index, InteractionAction action); + + + /** + * Clears all actions from this NPC + */ + void clearActions(); + + /** + * Gets if this NPC is visible to a player + * @param player The {@link Player} to check + * @return If this NPC is visible to the player + */ + boolean isVisibleTo(Player player); + + /** + * Hides this NPC from a player + * @param player The {@link Player} to hide from + */ + void hide(Player player); + + /** + * Shows this NPC to a player + * @param player The {@link Player} to show to + * @return A future that completes when the npc is fully shown to the player + */ + CompletableFuture show(Player player); + + /** + * Respawns this NPC for a player + * @param player The {@link Player} to respawn for + * @return A future that completes when the npc is fully respawned + */ + CompletableFuture respawn(Player player); + + /** + * Sets the head rotation of this NPC for a player + * @param player The {@link Player} to set the head rotation for + * @param yaw The yaw to set + * @param pitch The pitch to set + */ + void setHeadRotation(Player player, float yaw, float pitch); + + /** + * Sets the head rotation of this NPC for all players/viewers + * @param yaw The yaw to set + * @param pitch The pitch to set + */ + void setHeadRotation(float yaw, float pitch); + + /** + * @return The entity id of the packet entity that this npc object represents + */ + int getPacketEntityId(); + + /** + * @return The set of players that can currently see this npc + */ + Set getViewers(); + + /** + * Swings the entity's hand + * @param offHand Should the hand be the offhand + */ + void swingHand(boolean offHand); + + /** + * Gets the passengers of this npc + * @return The list of entity ids of the passengers + */ + + @Nullable List getPassengers(); + + /** + * Adds a passenger to this npc + * @param entityId The entity id of the passenger to add + */ + void addPassenger(int entityId); + + /** + * Removes a passenger from this npc + * @param entityId The entity id of the passenger to remove + */ + void removePassenger(int entityId); + + /** + * Gets the vehicle entity id of this npc + * @return The entity id of the vehicle + */ + @Nullable Integer getVehicleId(); + + /** + * Sets the vehicle id of this npc + * @param vehicleId The entity id of the vehicle + */ + void setVehicleId(Integer vehicleId); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcEntry.java b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcEntry.java new file mode 100644 index 0000000..aca4bc3 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcEntry.java @@ -0,0 +1,56 @@ +package lol.pyr.znpcsplus.api.npc; + +/** + * Base class for all NPC entries + * An NPC entry is a wrapper around an NPC that contains additional information + */ +public interface NpcEntry { + /** + * Gets the ID of this NPC entry + * @return The ID of this NPC entry + */ + String getId(); + /** + * Gets the NPC of this NPC entry + * @return The {@link Npc} of this NPC entry + */ + Npc getNpc(); + + /** + * Gets if this NPC entry is processed or not + * @return If this NPC entry is processed or not + */ + boolean isProcessed(); + /** + * Sets if this NPC entry is processed or not + * @param value If this NPC entry is processed or not + */ + void setProcessed(boolean value); + + /** + * @return If this NPC entry SHOULD be saved into the storage or not + */ + boolean isSave(); + /** + * Sets if this NPC should be saved or not + * @param value If this NPC entry should be saved or not + */ + void setSave(boolean value); + + /** + * Gets if this NPC can be modified by commands + * @return {@code true} if this NPC can be modified by commands, {@code false} otherwise + */ + boolean isAllowCommandModification(); + /** + * Sets if this NPC can be modified by commands + * @param value {@code true} if this NPC can be modified by commands, {@code false} otherwise + */ + void setAllowCommandModification(boolean value); + + /** + * Enables everything for this NPC entry + * That is, it makes the NPC processed, saveable, and allows command modification + */ + void enableEverything(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcRegistry.java b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcRegistry.java new file mode 100644 index 0000000..2d41981 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcRegistry.java @@ -0,0 +1,90 @@ +package lol.pyr.znpcsplus.api.npc; + +import lol.pyr.znpcsplus.util.NpcLocation; +import org.bukkit.World; + +import java.util.Collection; +import java.util.UUID; + +/** + * Base class for all NPC registries + */ +public interface NpcRegistry { + + /** + * Gets all NPC entries + * @return All NPC entries + */ + Collection getAll(); + + /** + * Gets all NPC IDs + * @return All NPC IDs + */ + Collection getAllIds(); + + /** + * Gets all NPC entries that are player made + * @return All player made NPC entries + */ + Collection getAllPlayerMade(); + + /** + * Gets IDs of all player made NPCs + * @return IDs of all player made NPCs + */ + Collection getAllPlayerMadeIds(); + + /** + * Creates a new NPC entry + * @param id The ID of the NPC entry + * @param world The {@link World} of the NPC entry + * @param type The {@link NpcType} of the NPC entry + * @param location The {@link NpcLocation} of the NPC entry + * @return The entry of the newly created npc + */ + NpcEntry create(String id, World world, NpcType type, NpcLocation location); + + /** + * Gets an NPC entry by its ID + * @param id The ID of the NPC entry + * @return The NPC entry + */ + NpcEntry getById(String id); + + /** + * Gets an NPC entry by its UUID + * @param uuid The UUID of the NPC entry + * @return The NPC entry + */ + NpcEntry getByUuid(UUID uuid); + + /** + * Deletes an NPC entry by its ID + * @param id The ID of the NPC entry + */ + void delete(String id); + + /** + * Deletes an NPC entry by its UUID + * @param uuid The UUID of the NPC entry + */ + void delete(UUID uuid); + + /** + * Register an NPC to this registry + * NpcEntry instances can be obtained through the NpcSerializer classes + * @param entry The npc to be registered + */ + void register(NpcEntry entry); + + /** + * Reload all saveable npcs from storage + */ + void reload(); + + /** + * Save all saveable npcs to storage + */ + void save(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcType.java b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcType.java new file mode 100644 index 0000000..004f512 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcType.java @@ -0,0 +1,29 @@ +package lol.pyr.znpcsplus.api.npc; + +import lol.pyr.znpcsplus.api.entity.EntityProperty; + +import java.util.Set; + +/** + * Represents a type of NPC. + * This defines the {@link org.bukkit.entity.EntityType} of the NPC, as well as the properties that are allowed to be set on the NPC. + */ +public interface NpcType { + /** + * The name of the NPC type. + * @return The name of the NPC type. + */ + String getName(); + + /** + * The offset of the hologram above the NPC. + * @return the offset + */ + double getHologramOffset(); + + /** + * Set of properties that are allowed to be set on the NPC. + * @return allowed properties + */ + Set> getAllowedProperties(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcTypeRegistry.java b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcTypeRegistry.java new file mode 100644 index 0000000..b0720db --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcTypeRegistry.java @@ -0,0 +1,21 @@ +package lol.pyr.znpcsplus.api.npc; + +import java.util.Collection; + +/** + * Base for NpcType registries. + */ +public interface NpcTypeRegistry { + /** + * Gets a NPC type by name. + * @param name The name of the NPC type. + * @return The type that is represented by the name or null if it doesnt exist + */ + NpcType getByName(String name); + + /** + * Gets all NPC types. + * @return all of the npc types + */ + Collection getAll(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/serialization/NpcSerializer.java b/api/src/main/java/lol/pyr/znpcsplus/api/serialization/NpcSerializer.java new file mode 100644 index 0000000..f35cbed --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/serialization/NpcSerializer.java @@ -0,0 +1,20 @@ +package lol.pyr.znpcsplus.api.serialization; + +import lol.pyr.znpcsplus.api.npc.NpcEntry; + +public interface NpcSerializer { + /** + * Serialize an npc into the type of this serializer + * @param entry The npc entry + * @return The serialized class + */ + T serialize(NpcEntry entry); + + /** + * Deserialize an npc from a serialized class + * Note: This npc will not be registered, you need to also register it using the NpcRegistry#register(NpcEntry) method + * @param model The serialized class + * @return The deserialized NpcEntry + */ + NpcEntry deserialize(T model); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/serialization/NpcSerializerRegistry.java b/api/src/main/java/lol/pyr/znpcsplus/api/serialization/NpcSerializerRegistry.java new file mode 100644 index 0000000..74e8d89 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/serialization/NpcSerializerRegistry.java @@ -0,0 +1,19 @@ +package lol.pyr.znpcsplus.api.serialization; + +public interface NpcSerializerRegistry { + /** + * Get an NpcSerializer that serializes npcs into the provided class + * @param clazz The class to serialize into + * @return The npc serializer instance + * @param The type of the class that the serializer serializes into + */ + NpcSerializer getSerializer(Class clazz); + + /** + * Register an NpcSerializer to be used by other plugins + * @param clazz The class that the serializer serializes into + * @param serializer The serializer itself + * @param The type of the class that the serializer serializes into + */ + void registerSerializer(Class clazz, NpcSerializer serializer); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/skin/Skin.java b/api/src/main/java/lol/pyr/znpcsplus/api/skin/Skin.java new file mode 100644 index 0000000..53ed651 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/skin/Skin.java @@ -0,0 +1,6 @@ +package lol.pyr.znpcsplus.api.skin; + +public interface Skin { + String getTexture(); + String getSignature(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptor.java b/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptor.java new file mode 100644 index 0000000..2e93207 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptor.java @@ -0,0 +1,11 @@ +package lol.pyr.znpcsplus.api.skin; + +import org.bukkit.entity.Player; + +import java.util.concurrent.CompletableFuture; + +public interface SkinDescriptor { + CompletableFuture fetch(Player player); + Skin fetchInstant(Player player); + boolean supportsInstant(Player player); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptorFactory.java b/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptorFactory.java new file mode 100644 index 0000000..46d979a --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptorFactory.java @@ -0,0 +1,18 @@ +package lol.pyr.znpcsplus.api.skin; + +import java.net.URL; +import java.util.UUID; + +/** + * Factory for creating skin descriptors. + */ +public interface SkinDescriptorFactory { + SkinDescriptor createMirrorDescriptor(); + SkinDescriptor createRefreshingDescriptor(String playerName); + SkinDescriptor createRefreshingDescriptor(UUID playerUUID); + SkinDescriptor createStaticDescriptor(String playerName); + SkinDescriptor createStaticDescriptor(String texture, String signature); + SkinDescriptor createUrlDescriptor(String url, String variant); + SkinDescriptor createUrlDescriptor(URL url, String variant); + SkinDescriptor createFileDescriptor(String path); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/ArmadilloState.java b/api/src/main/java/lol/pyr/znpcsplus/util/ArmadilloState.java new file mode 100644 index 0000000..bdea88f --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/ArmadilloState.java @@ -0,0 +1,8 @@ +package lol.pyr.znpcsplus.util; + +public enum ArmadilloState { + IDLE, + ROLLING, + SCARED, + UNROLLING +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/AttachDirection.java b/api/src/main/java/lol/pyr/znpcsplus/util/AttachDirection.java new file mode 100644 index 0000000..9322749 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/AttachDirection.java @@ -0,0 +1,10 @@ +package lol.pyr.znpcsplus.util; + +public enum AttachDirection { + DOWN, + UP, + NORTH, + SOUTH, + WEST, + EAST +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/AxolotlVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/AxolotlVariant.java new file mode 100644 index 0000000..b38b907 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/AxolotlVariant.java @@ -0,0 +1,9 @@ +package lol.pyr.znpcsplus.util; + +public enum AxolotlVariant { + LUCY, + WILD, + GOLD, + CYAN, + BLUE +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/BlockState.java b/api/src/main/java/lol/pyr/znpcsplus/util/BlockState.java new file mode 100644 index 0000000..6a4e41b --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/BlockState.java @@ -0,0 +1,14 @@ +package lol.pyr.znpcsplus.util; + +public class BlockState { + private final int globalId; + + public BlockState(int globalId) { + this.globalId = globalId; + } + + public int getGlobalId() { + return globalId; + } + +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/CatVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/CatVariant.java new file mode 100644 index 0000000..9aaa7f4 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/CatVariant.java @@ -0,0 +1,15 @@ +package lol.pyr.znpcsplus.util; + +public enum CatVariant { + TABBY, + BLACK, + RED, + SIAMESE, + BRITISH_SHORTHAIR, + CALICO, + PERSIAN, + RAGDOLL, + WHITE, + JELLIE, + ALL_BLACK +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/CreeperState.java b/api/src/main/java/lol/pyr/znpcsplus/util/CreeperState.java new file mode 100644 index 0000000..2b6a118 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/CreeperState.java @@ -0,0 +1,16 @@ +package lol.pyr.znpcsplus.util; + +public enum CreeperState { + IDLE(-1), + FUSE(1); + + private final int state; + + CreeperState(int state) { + this.state = state; + } + + public int getState() { + return state; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/FoxVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/FoxVariant.java new file mode 100644 index 0000000..5905cdb --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/FoxVariant.java @@ -0,0 +1,6 @@ +package lol.pyr.znpcsplus.util; + +public enum FoxVariant { + RED, + SNOW +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/FrogVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/FrogVariant.java new file mode 100644 index 0000000..53eaa5c --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/FrogVariant.java @@ -0,0 +1,7 @@ +package lol.pyr.znpcsplus.util; + +public enum FrogVariant { + TEMPERATE, + WARM, + COLD +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/HorseArmor.java b/api/src/main/java/lol/pyr/znpcsplus/util/HorseArmor.java new file mode 100644 index 0000000..80fa2f2 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/HorseArmor.java @@ -0,0 +1,8 @@ +package lol.pyr.znpcsplus.util; + +public enum HorseArmor { + NONE, + IRON, + GOLD, + DIAMOND +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/HorseColor.java b/api/src/main/java/lol/pyr/znpcsplus/util/HorseColor.java new file mode 100644 index 0000000..ae6327a --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/HorseColor.java @@ -0,0 +1,11 @@ +package lol.pyr.znpcsplus.util; + +public enum HorseColor { + WHITE, + CREAMY, + CHESTNUT, + BROWN, + BLACK, + GRAY, + DARK_BROWN +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/HorseStyle.java b/api/src/main/java/lol/pyr/znpcsplus/util/HorseStyle.java new file mode 100644 index 0000000..b5544c4 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/HorseStyle.java @@ -0,0 +1,9 @@ +package lol.pyr.znpcsplus.util; + +public enum HorseStyle { + NONE, + WHITE, + WHITEFIELD, + WHITE_DOTS, + BLACK_DOTS +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/HorseType.java b/api/src/main/java/lol/pyr/znpcsplus/util/HorseType.java new file mode 100644 index 0000000..9526d67 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/HorseType.java @@ -0,0 +1,9 @@ +package lol.pyr.znpcsplus.util; + +public enum HorseType { + HORSE, + DONKEY, + MULE, + ZOMBIE, + SKELETON +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/LlamaVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/LlamaVariant.java new file mode 100644 index 0000000..be5cdc9 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/LlamaVariant.java @@ -0,0 +1,8 @@ +package lol.pyr.znpcsplus.util; + +public enum LlamaVariant { + CREAMY, + WHITE, + BROWN, + GRAY +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/LookType.java b/api/src/main/java/lol/pyr/znpcsplus/util/LookType.java new file mode 100644 index 0000000..e6af244 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/LookType.java @@ -0,0 +1,7 @@ +package lol.pyr.znpcsplus.util; + +public enum LookType { + FIXED, + CLOSEST_PLAYER, + PER_PLAYER +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/MooshroomVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/MooshroomVariant.java new file mode 100644 index 0000000..f79fa0b --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/MooshroomVariant.java @@ -0,0 +1,10 @@ +package lol.pyr.znpcsplus.util; + +public enum MooshroomVariant { + RED, + BROWN; + + public static String getVariantName(MooshroomVariant variant) { + return variant.name().toLowerCase(); + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/NamedColor.java b/api/src/main/java/lol/pyr/znpcsplus/util/NamedColor.java new file mode 100644 index 0000000..42147d6 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/NamedColor.java @@ -0,0 +1,20 @@ +package lol.pyr.znpcsplus.util; + +public enum NamedColor { + BLACK, + DARK_BLUE, + DARK_GREEN, + DARK_AQUA, + DARK_RED, + DARK_PURPLE, + GOLD, + GRAY, + DARK_GRAY, + BLUE, + GREEN, + AQUA, + RED, + LIGHT_PURPLE, + YELLOW, + WHITE +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/NpcLocation.java b/api/src/main/java/lol/pyr/znpcsplus/util/NpcLocation.java new file mode 100644 index 0000000..422258d --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/NpcLocation.java @@ -0,0 +1,112 @@ +package lol.pyr.znpcsplus.util; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.util.NumberConversions; + +import java.util.Objects; + +public class NpcLocation { + private final double x; + private final double y; + private final double z; + private final float yaw; + private final float pitch; + + public NpcLocation(double x, double y, double z, float yaw, float pitch) { + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + this.pitch = pitch; + } + + public NpcLocation(Location location) { + this(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); + } + + public double getX() { + return this.x; + } + + public int getBlockX() { + return (int) getX(); + } + + public double getY() { + return this.y; + } + + public int getBlockY() { + return (int) getY(); + } + + public double getZ() { + return this.z; + } + + public int getBlockZ() { + return (int) getZ(); + } + + public float getYaw() { + return this.yaw; + } + + public float getPitch() { + return this.pitch; + } + + public NpcLocation centered() { + return new NpcLocation(Math.floor(x) + 0.5, y, Math.floor(z) + 0.5, yaw, pitch); + } + + public Location toBukkitLocation(World world) { + return new Location(world, this.x, this.y, this.z, this.yaw, this.pitch); + } + + public NpcLocation withY(double y) { + return new NpcLocation(x, y, z, yaw, pitch); + } + + private static final double _2PI = 2 * Math.PI; + + public NpcLocation lookingAt(Location loc) { + return lookingAt(new NpcLocation(loc)); + } + + public NpcLocation lookingAt(NpcLocation loc) { + final double x = loc.getX() - this.x; + final double z = loc.getZ() - this.z; + final double y = loc.getY() - this.y; + + if (x == 0 && z == 0) return new NpcLocation(this.x, this.y, this.z, this.yaw, y > 0 ? -90 : 90); + + double x2 = NumberConversions.square(x); + double z2 = NumberConversions.square(z); + double xz = Math.sqrt(x2 + z2); + + double theta = Math.atan2(-x, z); + float yaw = (float) Math.toDegrees((theta + _2PI) % _2PI); + float pitch = (float) Math.toDegrees(Math.atan(-y / xz)); + + return new NpcLocation(this.x, this.y, this.z, yaw, pitch); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NpcLocation that = (NpcLocation) o; + return Double.compare(that.x, x) == 0 && + Double.compare(that.y, y) == 0 && + Double.compare(that.z, z) == 0 && + Float.compare(that.yaw, yaw) == 0 && + Float.compare(that.pitch, pitch) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(x, y, z, yaw, pitch); + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/NpcPose.java b/api/src/main/java/lol/pyr/znpcsplus/util/NpcPose.java new file mode 100644 index 0000000..605702e --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/NpcPose.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.util; + +// TODO: Seperate this out into multiple classes and multiple properties depending on the npc type +// TODO: For example USING_TONGUE is only for the frog type but its usable everywhere + +// TODO #2: Add some backwards compatibility to some of these, like for example CROUCHING can be done +// TODO #2: on older versions using the general Entity number 0 bitmask +public enum NpcPose { + STANDING, + FALL_FLYING, + SLEEPING, + SWIMMING, + SPIN_ATTACK, + CROUCHING, + LONG_JUMPING, + DYING, + CROAKING, + USING_TONGUE, + SITTING, + ROARING, + SNIFFING, + EMERGING, + DIGGING, + SLIDING, + SHOOTING, + INHALING, +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/OcelotType.java b/api/src/main/java/lol/pyr/znpcsplus/util/OcelotType.java new file mode 100644 index 0000000..593ec57 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/OcelotType.java @@ -0,0 +1,8 @@ +package lol.pyr.znpcsplus.util; + +public enum OcelotType { + OCELOT, + TUXEDO, + TABBY, + SIAMESE, +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/PandaGene.java b/api/src/main/java/lol/pyr/znpcsplus/util/PandaGene.java new file mode 100644 index 0000000..dcb3f47 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/PandaGene.java @@ -0,0 +1,11 @@ +package lol.pyr.znpcsplus.util; + +public enum PandaGene { + NORMAL, + LAZY, + WORRIED, + PLAYFUL, + BROWN, + WEAK, + AGGRESSIVE +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/ParrotVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/ParrotVariant.java new file mode 100644 index 0000000..82d56b1 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/ParrotVariant.java @@ -0,0 +1,9 @@ +package lol.pyr.znpcsplus.util; + +public enum ParrotVariant { + RED_BLUE, + BLUE, + GREEN, + YELLOW_BLUE, + GRAY +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/PuffState.java b/api/src/main/java/lol/pyr/znpcsplus/util/PuffState.java new file mode 100644 index 0000000..0243932 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/PuffState.java @@ -0,0 +1,7 @@ +package lol.pyr.znpcsplus.util; + +public enum PuffState { + DEFLATED, + HALF_INFLATED, + FULLY_INFLATED, +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/RabbitType.java b/api/src/main/java/lol/pyr/znpcsplus/util/RabbitType.java new file mode 100644 index 0000000..fbdb47f --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/RabbitType.java @@ -0,0 +1,22 @@ +package lol.pyr.znpcsplus.util; + +public enum RabbitType { + BROWN(0), + WHITE(1), + BLACK(2), + BLACK_AND_WHITE(3), + GOLD(4), + SALT_AND_PEPPER(5), + THE_KILLER_BUNNY(99), + TOAST(100); + + private final int id; + + RabbitType(int id) { + this.id = id; + } + + public int getId() { + return id; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/SkeletonType.java b/api/src/main/java/lol/pyr/znpcsplus/util/SkeletonType.java new file mode 100644 index 0000000..ba3f05f --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/SkeletonType.java @@ -0,0 +1,11 @@ +package lol.pyr.znpcsplus.util; + +public enum SkeletonType { + NORMAL, + WITHER, + STRAY; + + public byte getLegacyId() { + return (byte) ordinal(); + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/SnifferState.java b/api/src/main/java/lol/pyr/znpcsplus/util/SnifferState.java new file mode 100644 index 0000000..a532947 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/SnifferState.java @@ -0,0 +1,11 @@ +package lol.pyr.znpcsplus.util; + +public enum SnifferState { + IDLING, + FEELING_HAPPY, + SCENTING, + SNIFFING, + SEARCHING, + DIGGING, + RISING +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/SpellType.java b/api/src/main/java/lol/pyr/znpcsplus/util/SpellType.java new file mode 100644 index 0000000..cf67c3b --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/SpellType.java @@ -0,0 +1,10 @@ +package lol.pyr.znpcsplus.util; + +public enum SpellType { + NONE, + SUMMON_VEX, + ATTACK, + WOLOLO, + DISAPPEAR, + BLINDNESS, +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/TropicalFishVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/TropicalFishVariant.java new file mode 100644 index 0000000..e201886 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/TropicalFishVariant.java @@ -0,0 +1,104 @@ +package lol.pyr.znpcsplus.util; + +import org.bukkit.DyeColor; + +import java.util.function.IntFunction; + +public class TropicalFishVariant { + + private final TropicalFishPattern pattern; + private final DyeColor bodyColor; + private final DyeColor patternColor; + + public TropicalFishVariant(TropicalFishPattern pattern, DyeColor bodyColor, DyeColor patternColor) { + this.pattern = pattern; + this.bodyColor = bodyColor; + this.patternColor = patternColor; + } + + public int getVariant() { + return pattern.getId() & '\uffff' | (bodyColor.ordinal() & 255) << 16 | (patternColor.ordinal() & 255) << 24; + } + + public enum TropicalFishPattern { + KOB(0, 0), + SUNSTREAK(0, 1), + SNOOPER(0, 2), + DASHER(0, 3), + BRINELY(0, 4), + SPOTTY(0, 5), + FLOPPER(1, 0), + STRIPEY(1, 1), + GLITTER(1, 2), + BLOCKFISH(1, 3), + BETTY(1, 4), + CLAYFISH(1, 5); + + private final int size; + private final int id; + private static final IntFunction BY_ID = (id) -> { + for (TropicalFishPattern pattern : values()) { + if (pattern.id == id) { + return pattern; + } + } + return null; + }; + + TropicalFishPattern(int size, int pattern) { + this.size = size; + this.id = size | pattern << 8; + } + + public int getSize() { + return size; + } + + public int getId() { + return id; + } + + public static TropicalFishPattern fromVariant(int variant) { + return BY_ID.apply(variant & '\uffff'); + } + } + + public static class Builder { + private TropicalFishPattern pattern; + private DyeColor bodyColor; + private DyeColor patternColor; + + public Builder() { + this.pattern = TropicalFishPattern.KOB; + this.bodyColor = DyeColor.WHITE; + this.patternColor = DyeColor.WHITE; + } + + public Builder pattern(TropicalFishPattern pattern) { + this.pattern = pattern; + return this; + } + + public Builder bodyColor(DyeColor bodyColor) { + this.bodyColor = bodyColor; + return this; + } + + public Builder patternColor(DyeColor patternColor) { + this.patternColor = patternColor; + return this; + } + + public static Builder fromInt(int variant) { + Builder builder = new Builder(); + builder.pattern = TropicalFishPattern.fromVariant(variant); + builder.bodyColor = DyeColor.values()[(variant >> 16) & 0xFF]; + builder.patternColor = DyeColor.values()[(variant >> 24) & 0xFF]; + return builder; + } + + public TropicalFishVariant build() { + return new TropicalFishVariant(pattern, bodyColor, patternColor); + } + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/Vector3f.java b/api/src/main/java/lol/pyr/znpcsplus/util/Vector3f.java new file mode 100644 index 0000000..eb55cac --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/Vector3f.java @@ -0,0 +1,46 @@ +package lol.pyr.znpcsplus.util; + +public class Vector3f { + private final float x; + private final float y; + private final float z; + + public Vector3f() { + this.x = 0.0F; + this.y = 0.0F; + this.z = 0.0F; + } + + public Vector3f(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vector3f(String s) { + String[] split = s.split(","); + this.x = Float.parseFloat(split[0]); + this.y = Float.parseFloat(split[1]); + this.z = Float.parseFloat(split[2]); + } + + public float getX() { + return this.x; + } + + public float getY() { + return this.y; + } + + public float getZ() { + return this.z; + } + + public String toString() { + return this.x + "," + this.y + "," + this.z; + } + + public static Vector3f zero() { + return new Vector3f(); + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/Vector3i.java b/api/src/main/java/lol/pyr/znpcsplus/util/Vector3i.java new file mode 100644 index 0000000..ae6a933 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/Vector3i.java @@ -0,0 +1,46 @@ +package lol.pyr.znpcsplus.util; + +public class Vector3i { + private final int x; + private final int y; + private final int z; + + public Vector3i(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + public int getX() { + return this.x; + } + + public int getY() { + return this.y; + } + + public int getZ() { + return this.z; + } + + public String toString() { + return this.x + "," + this.y + "," + this.z; + } + + public String toPrettyString() { + return "(" + this.x + ", " + this.y + ", " + this.z + ")"; + } + + public static Vector3i fromString(String s) { + String[] split = s.split(","); + if (split.length < 3) { + return null; + } else { + try { + return new Vector3i(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2])); + } catch (NumberFormatException var3) { + return null; + } + } + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/VillagerLevel.java b/api/src/main/java/lol/pyr/znpcsplus/util/VillagerLevel.java new file mode 100644 index 0000000..c1d481f --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/VillagerLevel.java @@ -0,0 +1,9 @@ +package lol.pyr.znpcsplus.util; + +public enum VillagerLevel { + STONE, + IRON, + GOLD, + EMERALD, + DIAMOND +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/VillagerProfession.java b/api/src/main/java/lol/pyr/znpcsplus/util/VillagerProfession.java new file mode 100644 index 0000000..939f425 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/VillagerProfession.java @@ -0,0 +1,40 @@ +package lol.pyr.znpcsplus.util; + +public enum VillagerProfession { + NONE(0, 0), + ARMORER(1, 3), + BUTCHER(2, 4), + CARTOGRAPHER(3, 1), + CLERIC(4, 2), + FARMER(5, 0), + FISHERMAN(6, 0), + FLETCHER(7, 0), + LEATHER_WORKER(8, 4), + LIBRARIAN(9, 1), + MASON(10), + NITWIT(11, 5), + SHEPHERD(12, 0), + TOOL_SMITH(13, 3), + WEAPON_SMITH(14, 3); + + private final int id; + private final int legacyId; + + VillagerProfession(int id) { + this.id = id; + this.legacyId = 0; + } + + VillagerProfession(int id, int legacyId) { + this.id = id; + this.legacyId = legacyId; + } + + public int getId() { + return id; + } + + public int getLegacyId() { + return legacyId; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/VillagerType.java b/api/src/main/java/lol/pyr/znpcsplus/util/VillagerType.java new file mode 100644 index 0000000..77623ff --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/VillagerType.java @@ -0,0 +1,20 @@ +package lol.pyr.znpcsplus.util; + +public enum VillagerType { + DESERT(0), + JUNGLE(1), + PLAINS(2), + SAVANNA(3), + SNOW(4), + SWAMP(5), + TAIGA(6); + private final int id; + + VillagerType(int id) { + this.id = id; + } + + public int getId() { + return id; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/WoldVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/WoldVariant.java new file mode 100644 index 0000000..fed5d66 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/WoldVariant.java @@ -0,0 +1,23 @@ +package lol.pyr.znpcsplus.util; + +public enum WoldVariant { + PALE(3), + SPOTTED(6), + SNOWY(5), + BLACK(1), + ASHEN(0), + RUSTY(4), + WOODS(8), + CHESTNUT(2), + STRIPED(7); + + private final int id; + + WoldVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/ZombieType.java b/api/src/main/java/lol/pyr/znpcsplus/util/ZombieType.java new file mode 100644 index 0000000..8751b82 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/ZombieType.java @@ -0,0 +1,11 @@ +package lol.pyr.znpcsplus.util; + +public enum ZombieType { + ZOMBIE, + FARMER, + LIBRARIAN, + PRIEST, + BLACKSMITH, + BUTCHER, + HUSK +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..39cfcbd --- /dev/null +++ b/build.gradle @@ -0,0 +1,37 @@ +subprojects { + apply plugin: "java" + + group "lol.pyr" + version "2.1.0" + (System.getenv().containsKey("BUILD_ID") ? "-SNAPSHOT" : "") + + java { + toolchain.languageVersion.set(JavaLanguageVersion.of(8)) + } + + dependencies { + compileOnly "org.jetbrains:annotations:26.0.1" + compileOnly "org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT" + } + + repositories { + mavenCentral() + maven { + url "https://hub.spigotmc.org/nexus/content/repositories/snapshots/" + } + maven { + url "https://repo.codemc.io/repository/maven-releases/" + } + maven { + url "https://libraries.minecraft.net" + } + maven { + url "https://repo.papermc.io/repository/maven-public/" + } + maven { + url "https://repo.extendedclip.com/content/repositories/placeholderapi/" + } + maven { + url "https://repo.pyr.lol/releases" + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..41d9927 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a595206 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/plugin/build.gradle b/plugin/build.gradle new file mode 100644 index 0000000..2d3def9 --- /dev/null +++ b/plugin/build.gradle @@ -0,0 +1,71 @@ +plugins { + id "java" + id "com.github.johnrengelman.shadow" version "8.1.1" + id "xyz.jpenilla.run-paper" version "2.2.0" +} + +runServer { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(21) + } + minecraftVersion "1.21.4" +} + +processResources { + expand("version": version) +} + +dependencies { + compileOnly "me.clip:placeholderapi:2.11.6" // Placeholder support + implementation "com.google.code.gson:gson:2.10.1" // JSON parsing + implementation "org.bstats:bstats-bukkit:3.0.2" // Plugin stats + implementation "com.github.retrooper:packetevents-spigot:2.9.1" // Packets + implementation "space.arim.dazzleconf:dazzleconf-ext-snakeyaml:1.2.1" // Configs + implementation "lol.pyr:director-adventure:2.1.2" // Commands + + // Fancy text library + implementation "net.kyori:adventure-platform-bukkit:4.3.4" + implementation "net.kyori:adventure-text-minimessage:4.17.0" + + implementation project(":api") +} + +ext { + gitBranch = System.getenv('GIT_BRANCH') ?: '' + gitCommitHash = System.getenv('GIT_COMMIT') ?: '' + buildId = System.getenv('BUILD_ID') ?: '' +} + +shadowJar { + archivesBaseName = "ZNPCsPlus" + archiveClassifier.set "" + + manifest { + if (gitBranch?.trim()) { + attributes('Git-Branch': gitBranch) + } + if (gitCommitHash?.trim()) { + attributes('Git-Commit': gitCommitHash) + } + if (buildId?.trim()) { + attributes('Build-Id': buildId) + } + } + + relocate "org.objectweb.asm", "lol.pyr.znpcsplus.libraries.asm" + relocate "me.lucko.jarrelocator", "lol.pyr.znpcsplus.libraries.jarrelocator" + + relocate "org.bstats", "lol.pyr.znpcsplus.libraries.bstats" + relocate "net.kyori", "lol.pyr.znpcsplus.libraries.kyori" + relocate "org.checkerframework", "lol.pyr.znpcsplus.libraries.checkerframework" + relocate "com.google.gson", "lol.pyr.znpcsplus.libraries.gson" + relocate "com.github.retrooper.packetevents", "lol.pyr.znpcsplus.libraries.packetevents.api" + relocate "io.github.retrooper.packetevents", "lol.pyr.znpcsplus.libraries.packetevents.impl" + relocate "org.yaml.snakeyaml", "lol.pyr.znpcsplus.libraries.snakeyaml" + relocate "space.arim.dazzleconf", "lol.pyr.znpcsplus.libraries.dazzleconf" + relocate "lol.pyr.director", "lol.pyr.znpcsplus.libraries.command" + + minimize() +} + +tasks.assemble.dependsOn shadowJar diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java new file mode 100644 index 0000000..4582c95 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java @@ -0,0 +1,361 @@ +package lol.pyr.znpcsplus; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.event.PacketListenerPriority; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder; +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandManager; +import lol.pyr.director.adventure.command.MultiCommand; +import lol.pyr.director.adventure.parse.primitive.BooleanParser; +import lol.pyr.director.adventure.parse.primitive.DoubleParser; +import lol.pyr.director.adventure.parse.primitive.FloatParser; +import lol.pyr.director.adventure.parse.primitive.IntegerParser; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.api.NpcApiProvider; +import lol.pyr.znpcsplus.api.NpcPropertyRegistryProvider; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.commands.*; +import lol.pyr.znpcsplus.commands.action.*; +import lol.pyr.znpcsplus.commands.hologram.*; +import lol.pyr.znpcsplus.commands.property.PropertyRemoveCommand; +import lol.pyr.znpcsplus.commands.property.PropertySetCommand; +import lol.pyr.znpcsplus.commands.storage.ImportCommand; +import lol.pyr.znpcsplus.commands.storage.LoadAllCommand; +import lol.pyr.znpcsplus.commands.storage.MigrateCommand; +import lol.pyr.znpcsplus.commands.storage.SaveAllCommand; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.conversion.DataImporterRegistry; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.interaction.ActionFactoryImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.interaction.InteractionPacketListener; +import lol.pyr.znpcsplus.npc.*; +import lol.pyr.znpcsplus.packets.*; +import lol.pyr.znpcsplus.parsers.*; +import lol.pyr.znpcsplus.scheduling.FoliaScheduler; +import lol.pyr.znpcsplus.scheduling.SpigotScheduler; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.serialization.NpcSerializerRegistryImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.skin.cache.SkinCacheCleanTask; +import lol.pyr.znpcsplus.storage.NpcStorageType; +import lol.pyr.znpcsplus.tasks.HologramRefreshTask; +import lol.pyr.znpcsplus.tasks.NpcProcessorTask; +import lol.pyr.znpcsplus.tasks.ViewableHideOnLeaveListener; +import lol.pyr.znpcsplus.updater.UpdateChecker; +import lol.pyr.znpcsplus.updater.UpdateNotificationListener; +import lol.pyr.znpcsplus.user.ClientPacketListener; +import lol.pyr.znpcsplus.user.UserListener; +import lol.pyr.znpcsplus.user.UserManager; +import lol.pyr.znpcsplus.util.*; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +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.bstats.bukkit.Metrics; +import org.bukkit.*; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.PluginManager; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; + +public class ZNpcsPlus { + private final LegacyComponentSerializer textSerializer = LegacyComponentSerializer.builder() + .character('&') + .hexCharacter('#') + .hexColors().build(); + + private final List shutdownTasks = new ArrayList<>(); + private final PacketEventsAPI packetEvents; + private final ZNpcsPlusBootstrap bootstrap; + + private final ConfigManager configManager; + private final MojangSkinCache skinCache; + private final EntityPropertyRegistryImpl propertyRegistry; + + public ZNpcsPlus(ZNpcsPlusBootstrap bootstrap) { + this.bootstrap = bootstrap; + packetEvents = SpigotPacketEventsBuilder.build(bootstrap); + PacketEvents.setAPI(packetEvents); + packetEvents.getSettings().checkForUpdates(false); + packetEvents.load(); + + configManager = new ConfigManager(getDataFolder()); + skinCache = new MojangSkinCache(configManager, new File(getDataFolder(), "skins")); + propertyRegistry = new EntityPropertyRegistryImpl(skinCache, configManager); + + NpcPropertyRegistryProvider.register(bootstrap, propertyRegistry); + shutdownTasks.add(NpcPropertyRegistryProvider::unregister); + } + + private void log(String str) { + Bukkit.getConsoleSender().sendMessage(str); + } + + public void onEnable() { + getDataFolder().mkdirs(); + + log(ChatColor.YELLOW + " ___ __ __ __"); + log(ChatColor.YELLOW + " _/ |\\ | |__) | (__` " + ChatColor.GOLD + "__|__ " + ChatColor.YELLOW + getDescription().getName() + " " + ChatColor.GOLD + "v" + getDescription().getVersion()); + log(ChatColor.YELLOW + " /__ | \\| | |__ .__) " + ChatColor.GOLD + " | " + ChatColor.GRAY + "Maintained with " + ChatColor.RED + "\u2764 " + ChatColor.GRAY + " by Pyr#6969"); + log(""); + + PluginManager pluginManager = Bukkit.getPluginManager(); + long before = System.currentTimeMillis(); + + log(ChatColor.WHITE + " * Initializing libraries..."); + + packetEvents.init(); + + BukkitAudiences adventure = BukkitAudiences.create(bootstrap); + shutdownTasks.add(adventure::close); + + log(ChatColor.WHITE + " * Initializing components..."); + + TaskScheduler scheduler = FoliaUtil.isFolia() ? new FoliaScheduler(bootstrap) : new SpigotScheduler(bootstrap); + shutdownTasks.add(scheduler::cancelAll); + + + PacketFactory packetFactory = setupPacketFactory(scheduler, propertyRegistry, configManager); + propertyRegistry.registerTypes(packetFactory, textSerializer, scheduler); + + BungeeConnector bungeeConnector = new BungeeConnector(bootstrap); + ActionRegistryImpl actionRegistry = new ActionRegistryImpl(); + ActionFactoryImpl actionFactory = new ActionFactoryImpl(scheduler, adventure, textSerializer, bungeeConnector); + NpcTypeRegistryImpl typeRegistry = new NpcTypeRegistryImpl(); + NpcSerializerRegistryImpl serializerRegistry = new NpcSerializerRegistryImpl(packetFactory, configManager, actionRegistry, typeRegistry, propertyRegistry, textSerializer); + NpcRegistryImpl npcRegistry = new NpcRegistryImpl(configManager, this, packetFactory, actionRegistry, + scheduler, typeRegistry, propertyRegistry, serializerRegistry, textSerializer); + shutdownTasks.add(npcRegistry::unload); + + UserManager userManager = new UserManager(); + shutdownTasks.add(userManager::shutdown); + + DataImporterRegistry importerRegistry = new DataImporterRegistry(configManager, adventure, + scheduler, packetFactory, textSerializer, typeRegistry, getDataFolder().getParentFile(), + propertyRegistry, skinCache, npcRegistry, bungeeConnector); + + log(ChatColor.WHITE + " * Registering components..."); + + bungeeConnector.registerChannel(); + shutdownTasks.add(bungeeConnector::unregisterChannel); + + typeRegistry.registerDefault(packetEvents, propertyRegistry); + actionRegistry.registerTypes(scheduler, adventure, textSerializer, bungeeConnector); + packetEvents.getEventManager().registerListener(new InteractionPacketListener(userManager, npcRegistry, typeRegistry, scheduler), PacketListenerPriority.MONITOR); + packetEvents.getEventManager().registerListener(new ClientPacketListener(configManager), PacketListenerPriority.LOWEST); + new Metrics(bootstrap, 18244); + pluginManager.registerEvents(new UserListener(userManager), bootstrap); + + registerCommands(npcRegistry, skinCache, adventure, actionRegistry, + typeRegistry, propertyRegistry, importerRegistry, configManager, packetFactory, serializerRegistry); + + log(ChatColor.WHITE + " * Starting tasks..."); + if (configManager.getConfig().checkForUpdates()) { + UpdateChecker updateChecker = new UpdateChecker(getDescription()); + scheduler.runDelayedTimerAsync(updateChecker, 5L, 6000L); + pluginManager.registerEvents(new UpdateNotificationListener(this, adventure, updateChecker, scheduler), bootstrap); + } + + scheduler.runDelayedTimerAsync(new NpcProcessorTask(npcRegistry, propertyRegistry, userManager), 60L, 3L); + scheduler.runDelayedTimerAsync(new HologramRefreshTask(npcRegistry), 60L, 20L); + scheduler.runDelayedTimerAsync(new SkinCacheCleanTask(skinCache), 1200, 1200); + pluginManager.registerEvents(new ViewableHideOnLeaveListener(), bootstrap); + + log(ChatColor.WHITE + " * Loading data..."); + npcRegistry.reload(); + if (configManager.getConfig().autoSaveEnabled()) shutdownTasks.add(npcRegistry::save); + + if (bootstrap.movedLegacy()) { + log(ChatColor.WHITE + " * Converting legacy data..."); + try { + Collection entries = importerRegistry.getImporter("znpcsplus_legacy").importData(); + npcRegistry.registerAll(entries); + } catch (Exception exception) { + log(ChatColor.RED + " * Legacy data conversion failed! Check conversion.log for more info."); + try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(new File(getDataFolder(), "conversion.log").toPath(), StandardOpenOption.CREATE_NEW))) { + exception.printStackTrace(writer); + } catch (IOException e) { + log(ChatColor.DARK_RED + " * Critical error! Writing to conversion.log failed."); + e.printStackTrace(); + } + } + } + + NpcApiProvider.register(bootstrap, new ZNpcsPlusApi(npcRegistry, typeRegistry, propertyRegistry, actionRegistry, actionFactory, skinCache, serializerRegistry)); + log(ChatColor.WHITE + " * Loading complete! (" + (System.currentTimeMillis() - before) + "ms)"); + log(""); + + if (configManager.getConfig().debugEnabled()) { + World world = Bukkit.getWorld("world"); + if (world == null) world = Bukkit.getWorlds().get(0); + int i = 0; + for (NpcTypeImpl type : typeRegistry.getAllImpl()) { + NpcEntryImpl entry = npcRegistry.create("debug_npc_" + i, world, type, new NpcLocation(i * 3, 200, 0, 0, 0)); + entry.setProcessed(true); + NpcImpl npc = entry.getNpc(); + npc.getHologram().addTextLineComponent(Component.text("Hello, World!", TextColor.color(255, 0, 0))); + npc.getHologram().addTextLineComponent(Component.text("Hello, World!", TextColor.color(0, 255, 0))); + npc.getHologram().addTextLineComponent(Component.text("Hello, World!", TextColor.color(0, 0, 255))); + i++; + } + } + } + + public void onDisable() { + NpcApiProvider.unregister(); + for (Runnable runnable : shutdownTasks) try { + runnable.run(); + } catch (Throwable throwable) { + bootstrap.getLogger().severe("One of the registered shutdown tasks threw an exception:"); + throwable.printStackTrace(); + } + shutdownTasks.clear(); + PacketEvents.getAPI().terminate(); + } + + private PacketFactory setupPacketFactory(TaskScheduler scheduler, EntityPropertyRegistryImpl propertyRegistry, ConfigManager configManager) { + HashMap> versions = new HashMap<>(); + versions.put(ServerVersion.V_1_8, LazyLoader.of(() -> new V1_8PacketFactory(scheduler, packetEvents, propertyRegistry, textSerializer, configManager))); + versions.put(ServerVersion.V_1_17, LazyLoader.of(() -> new V1_17PacketFactory(scheduler, packetEvents, propertyRegistry, textSerializer, configManager))); + versions.put(ServerVersion.V_1_19_3, LazyLoader.of(() -> new V1_19_3PacketFactory(scheduler, packetEvents, propertyRegistry, textSerializer, configManager))); + versions.put(ServerVersion.V_1_20_2, LazyLoader.of(() -> new V1_20_2PacketFactory(scheduler, packetEvents, propertyRegistry, textSerializer, configManager))); + versions.put(ServerVersion.V_1_21_3, LazyLoader.of(() -> new V1_21_3PacketFactory(scheduler, packetEvents, propertyRegistry, textSerializer, configManager))); + versions.put(ServerVersion.V_1_21_7, LazyLoader.of(() -> new V1_21_7PacketFactory(scheduler, packetEvents, propertyRegistry, textSerializer, configManager))); + + ServerVersion version = packetEvents.getServerManager().getVersion(); + if (versions.containsKey(version)) return versions.get(version).get(); + for (ServerVersion v : ServerVersion.reversedValues()) { + if (v.isNewerThan(version)) continue; + if (!versions.containsKey(v)) continue; + return versions.get(v).get(); + } + throw new RuntimeException("Unsupported version!"); + } + + private void registerCommands(NpcRegistryImpl npcRegistry, MojangSkinCache skinCache, BukkitAudiences adventure, + ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, + EntityPropertyRegistryImpl propertyRegistry, DataImporterRegistry importerRegistry, + ConfigManager configManager, PacketFactory packetFactory, NpcSerializerRegistryImpl serializerRegistry) { + + Message incorrectUsageMessage = context -> context.send(Component.text("Incorrect usage: /" + context.getUsage(), NamedTextColor.RED)); + CommandManager manager = new CommandManager(bootstrap, adventure, incorrectUsageMessage); + + manager.registerParser(NpcTypeImpl.class, new NpcTypeParser(incorrectUsageMessage, typeRegistry)); + manager.registerParser(NpcEntryImpl.class, new NpcEntryParser(npcRegistry, incorrectUsageMessage)); + manager.registerParser(EntityPropertyImpl.class, new EntityPropertyParser(incorrectUsageMessage, propertyRegistry)); + manager.registerParser(Integer.class, new IntegerParser(incorrectUsageMessage)); + manager.registerParser(Double.class, new DoubleParser(incorrectUsageMessage)); + manager.registerParser(Float.class, new FloatParser(incorrectUsageMessage)); + manager.registerParser(Boolean.class, new BooleanParser(incorrectUsageMessage)); + manager.registerParser(NamedColor.class, new NamedColorParser(incorrectUsageMessage)); + manager.registerParser(InteractionType.class, new InteractionTypeParser(incorrectUsageMessage)); + manager.registerParser(Color.class, new ColorParser(incorrectUsageMessage)); + manager.registerParser(Vector3f.class, new Vector3fParser(incorrectUsageMessage)); + manager.registerParser(String.class, new StringParser(incorrectUsageMessage)); + manager.registerParser(Vector3i.class, new Vector3iParser(incorrectUsageMessage)); + + // TODO: Need to find a better way to do this + registerEnumParser(manager, NpcPose.class, incorrectUsageMessage); + registerEnumParser(manager, DyeColor.class, incorrectUsageMessage); + registerEnumParser(manager, CatVariant.class, incorrectUsageMessage); + registerEnumParser(manager, CreeperState.class, incorrectUsageMessage); + registerEnumParser(manager, ParrotVariant.class, incorrectUsageMessage); + registerEnumParser(manager, SpellType.class, incorrectUsageMessage); + registerEnumParser(manager, FoxVariant.class, incorrectUsageMessage); + registerEnumParser(manager, FrogVariant.class, incorrectUsageMessage); + registerEnumParser(manager, VillagerType.class, incorrectUsageMessage); + registerEnumParser(manager, VillagerProfession.class, incorrectUsageMessage); + registerEnumParser(manager, VillagerLevel.class, incorrectUsageMessage); + registerEnumParser(manager, AxolotlVariant.class, incorrectUsageMessage); + registerEnumParser(manager, HorseType.class, incorrectUsageMessage); + registerEnumParser(manager, HorseStyle.class, incorrectUsageMessage); + registerEnumParser(manager, HorseColor.class, incorrectUsageMessage); + registerEnumParser(manager, HorseArmor.class, incorrectUsageMessage); + registerEnumParser(manager, LlamaVariant.class, incorrectUsageMessage); + registerEnumParser(manager, MooshroomVariant.class, incorrectUsageMessage); + registerEnumParser(manager, OcelotType.class, incorrectUsageMessage); + registerEnumParser(manager, PandaGene.class, incorrectUsageMessage); + registerEnumParser(manager, PuffState.class, incorrectUsageMessage); + registerEnumParser(manager, LookType.class, incorrectUsageMessage); + registerEnumParser(manager, TropicalFishVariant.TropicalFishPattern.class, incorrectUsageMessage); + registerEnumParser(manager, SnifferState.class, incorrectUsageMessage); + registerEnumParser(manager, RabbitType.class, incorrectUsageMessage); + registerEnumParser(manager, AttachDirection.class, incorrectUsageMessage); + registerEnumParser(manager, Sound.class, incorrectUsageMessage); + registerEnumParser(manager, ArmadilloState.class, incorrectUsageMessage); + registerEnumParser(manager, WoldVariant.class, incorrectUsageMessage); + registerEnumParser(manager, NpcStorageType.class, incorrectUsageMessage); + registerEnumParser(manager, SkeletonType.class, incorrectUsageMessage); + + manager.registerCommand("npc", new MultiCommand(bootstrap.loadHelpMessage("root")) + .addSubcommand("center", new CenterCommand(npcRegistry)) + .addSubcommand("create", new CreateCommand(npcRegistry, typeRegistry)) + .addSubcommand("clone", new CloneCommand(npcRegistry)) + .addSubcommand("reloadconfig", new ReloadConfigCommand(configManager)) + .addSubcommand("toggle", new ToggleCommand(npcRegistry)) + .addSubcommand("skin", new SkinCommand(skinCache, npcRegistry, typeRegistry, propertyRegistry)) + .addSubcommand("delete", new DeleteCommand(npcRegistry, adventure)) + .addSubcommand("move", new MoveCommand(npcRegistry)) + .addSubcommand("teleport", new TeleportCommand(npcRegistry)) + .addSubcommand("list", new ListCommand(npcRegistry)) + .addSubcommand("near", new NearCommand(npcRegistry)) + .addSubcommand("type", new TypeCommand(npcRegistry, typeRegistry)) + .addSubcommand("setlocation", new SetLocationCommand(npcRegistry)) + .addSubcommand("lookatme", new LookAtMeCommand(npcRegistry)) + .addSubcommand("setrotation", new SetRotationCommand(npcRegistry)) + .addSubcommand("changeid", new ChangeIdCommand(npcRegistry)) + .addSubcommand("property", new MultiCommand(bootstrap.loadHelpMessage("property")) + .addSubcommand("set", new PropertySetCommand(npcRegistry)) + .addSubcommand("remove", new PropertyRemoveCommand(npcRegistry))) + .addSubcommand("storage", new MultiCommand(bootstrap.loadHelpMessage("storage")) + .addSubcommand("save", new SaveAllCommand(npcRegistry)) + .addSubcommand("reload", new LoadAllCommand(npcRegistry)) + .addSubcommand("import", new ImportCommand(npcRegistry, importerRegistry)) + .addSubcommand("migrate", new MigrateCommand(configManager, this, packetFactory, actionRegistry, typeRegistry, propertyRegistry, textSerializer, npcRegistry.getStorage(), configManager.getConfig().storageType(), npcRegistry, serializerRegistry))) + .addSubcommand("holo", new MultiCommand(bootstrap.loadHelpMessage("holo")) + .addSubcommand("add", new HoloAddCommand(npcRegistry)) + .addSubcommand("additem", new HoloAddItemCommand(npcRegistry)) + .addSubcommand("delete", new HoloDeleteCommand(npcRegistry)) + .addSubcommand("info", new HoloInfoCommand(npcRegistry)) + .addSubcommand("insert", new HoloInsertCommand(npcRegistry)) + .addSubcommand("insertitem", new HoloInsertItemCommand(npcRegistry)) + .addSubcommand("set", new HoloSetCommand(npcRegistry)) + .addSubcommand("setitem", new HoloSetItemCommand(npcRegistry)) + .addSubcommand("offset", new HoloOffsetCommand(npcRegistry)) + .addSubcommand("refreshdelay", new HoloRefreshDelayCommand(npcRegistry))) + .addSubcommand("action", new MultiCommand(bootstrap.loadHelpMessage("action")) + .addSubcommand("add", new ActionAddCommand(npcRegistry, actionRegistry)) + .addSubcommand("clear", new ActionClearCommand(npcRegistry)) + .addSubcommand("delete", new ActionDeleteCommand(npcRegistry)) + .addSubcommand("edit", new ActionEditCommand(npcRegistry, actionRegistry)) + .addSubcommand("list", new ActionListCommand(npcRegistry))) + .addSubcommand("version", new VersionCommand(this)) + ); + } + + private > void registerEnumParser(CommandManager manager, Class clazz, Message message) { + manager.registerParser(clazz, new EnumParser<>(clazz, message)); + } + + public File getDataFolder() { + return bootstrap.getDataFolder(); + } + + public PluginDescriptionFile getDescription() { + return bootstrap.getDescription(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusApi.java b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusApi.java new file mode 100644 index 0000000..7d272da --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusApi.java @@ -0,0 +1,73 @@ +package lol.pyr.znpcsplus; + +import lol.pyr.znpcsplus.api.NpcApi; +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.api.interaction.ActionFactory; +import lol.pyr.znpcsplus.api.interaction.ActionRegistry; +import lol.pyr.znpcsplus.api.npc.NpcRegistry; +import lol.pyr.znpcsplus.api.npc.NpcTypeRegistry; +import lol.pyr.znpcsplus.api.skin.SkinDescriptorFactory; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.interaction.ActionFactoryImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.serialization.NpcSerializerRegistryImpl; +import lol.pyr.znpcsplus.skin.SkinDescriptorFactoryImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; + +public class ZNpcsPlusApi implements NpcApi { + private final NpcRegistryImpl npcRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final ActionRegistryImpl actionRegistry; + private final ActionFactoryImpl actionFactory; + private final SkinDescriptorFactoryImpl skinDescriptorFactory; + private final NpcSerializerRegistryImpl npcSerializerRegistry; + + public ZNpcsPlusApi(NpcRegistryImpl npcRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, ActionRegistryImpl actionRegistry, ActionFactoryImpl actionFactory, MojangSkinCache skinCache, NpcSerializerRegistryImpl npcSerializerRegistry) { + this.npcRegistry = npcRegistry; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.actionRegistry = actionRegistry; + this.actionFactory = actionFactory; + this.skinDescriptorFactory = new SkinDescriptorFactoryImpl(skinCache); + this.npcSerializerRegistry = npcSerializerRegistry; + } + + @Override + public NpcRegistry getNpcRegistry() { + return npcRegistry; + } + + @Override + public NpcTypeRegistry getNpcTypeRegistry() { + return typeRegistry; + } + + @Override + public EntityPropertyRegistry getPropertyRegistry() { + return propertyRegistry; + } + + @Override + public ActionRegistry getActionRegistry() { + return actionRegistry; + } + + @Override + public ActionFactory getActionFactory() { + return actionFactory; + } + + + @Override + public SkinDescriptorFactory getSkinDescriptorFactory() { + return skinDescriptorFactory; + } + + @Override + public NpcSerializerRegistryImpl getNpcSerializerRegistry() { + return npcSerializerRegistry; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusBootstrap.java b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusBootstrap.java new file mode 100644 index 0000000..9eb1ab9 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusBootstrap.java @@ -0,0 +1,73 @@ +package lol.pyr.znpcsplus; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.util.FileUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.nio.file.Files; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ZNpcsPlusBootstrap extends JavaPlugin { + private ZNpcsPlus zNpcsPlus; + private boolean legacy; + + @Override + public void onLoad() { + legacy = new File(getDataFolder(), "data.json").isFile() && !new File(getDataFolder(), "data").isDirectory(); + if (legacy) try { + Files.move(getDataFolder().toPath(), new File(getDataFolder().getParentFile(), "ZNPCsPlusLegacy").toPath()); + } catch (IOException e) { + getLogger().severe(ChatColor.RED + "Failed to move legacy data folder! Plugin will disable."); + e.printStackTrace(); + Bukkit.getPluginManager().disablePlugin(this); + return; + } + zNpcsPlus = new ZNpcsPlus(this); + } + + @Override + public void onEnable() { + if (zNpcsPlus != null) zNpcsPlus.onEnable(); + } + + @Override + public void onDisable() { + if (zNpcsPlus != null) zNpcsPlus.onDisable(); + } + + private final static Pattern EMBEDDED_FILE_PATTERN = Pattern.compile("\\{@(.*?)}"); + + private String loadMessageFile(String file) { + Reader reader = getTextResource("messages/" + file + ".txt"); + if (reader == null) throw new RuntimeException(file + ".txt is missing from ZNPCsPlus jar!"); + String text = FileUtil.dumpReaderAsString(reader); + Matcher matcher = EMBEDDED_FILE_PATTERN.matcher(text); + StringBuilder builder = new StringBuilder(); + int lastMatchEnd = 0; + while (matcher.find()) { + builder.append(text, lastMatchEnd, matcher.start()); + lastMatchEnd = matcher.end(); + builder.append(loadMessageFile(matcher.group(1))); + } + builder.append(text, lastMatchEnd, text.length()); + return builder.toString(); + } + + protected Message loadHelpMessage(String name) { + Component component = MiniMessage.miniMessage().deserialize(loadMessageFile(name)); + return context -> context.send(component); + } + + public boolean movedLegacy() { + return legacy; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/CenterCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/CenterCommand.java new file mode 100644 index 0000000..b1a806d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/CenterCommand.java @@ -0,0 +1,35 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class CenterCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public CenterCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " center "); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + npc.setLocation(npc.getLocation().centered()); + context.send(Component.text("NPC has been centered on it's current block.", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/ChangeIdCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ChangeIdCommand.java new file mode 100644 index 0000000..1d64db6 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ChangeIdCommand.java @@ -0,0 +1,36 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class ChangeIdCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public ChangeIdCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " changeid "); + NpcEntryImpl old = context.parse(NpcEntryImpl.class); + String newId = context.popString(); + if (npcRegistry.getById(newId) != null) context.halt(Component.text("There is already an npc with the new id you have provided", NamedTextColor.RED)); + npcRegistry.switchIds(old.getId(), newId); + context.send(Component.text("Npc's id changed to " + newId.toLowerCase(), NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/CloneCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/CloneCommand.java new file mode 100644 index 0000000..eb9d001 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/CloneCommand.java @@ -0,0 +1,44 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; + +public class CloneCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public CloneCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " clone "); + Player player = context.ensureSenderIsPlayer(); + + String id = context.popString(); + if (npcRegistry.getById(id) == null) context.halt(Component.text("NPC with ID " + id + " does not exist.", NamedTextColor.RED)); + String newId = context.popString(); + if (npcRegistry.getById(newId) != null) context.halt(Component.text("NPC with ID " + newId + " already exists.", NamedTextColor.RED)); + + npcRegistry.clone(id, newId, player.getWorld(), new NpcLocation(player.getLocation())); + + context.send(Component.text("Cloned NPC with ID " + id + " to ID " + newId + ".", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/CreateCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/CreateCommand.java new file mode 100644 index 0000000..341edca --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/CreateCommand.java @@ -0,0 +1,54 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; + +public class CreateCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + private final NpcTypeRegistryImpl typeRegistry; + + public CreateCommand(NpcRegistryImpl npcRegistry, NpcTypeRegistryImpl typeRegistry) { + this.npcRegistry = npcRegistry; + this.typeRegistry = typeRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " create []"); + Player player = context.ensureSenderIsPlayer(); + + String id = context.popString(); + if (npcRegistry.getById(id) != null) context.halt(Component.text("NPC with that ID already exists.", NamedTextColor.RED)); + + NpcTypeImpl type; + if (context.argSize() == 1) { + type = context.parse(NpcTypeImpl.class); + } else { + type = typeRegistry.getByName("player"); + } + + NpcEntryImpl entry = npcRegistry.create(id, player.getWorld(), type, new NpcLocation(player.getLocation())); + entry.enableEverything(); + + context.send(Component.text("Created a " + type.getName() + " NPC with ID " + id + ".", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(typeRegistry.getAllImpl().stream().map(NpcTypeImpl::getName)); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/DeleteCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/DeleteCommand.java new file mode 100644 index 0000000..d22cc52 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/DeleteCommand.java @@ -0,0 +1,37 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class DeleteCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + private final BukkitAudiences adventure; + + public DeleteCommand(NpcRegistryImpl npcRegistry, BukkitAudiences adventure) { + this.npcRegistry = npcRegistry; + this.adventure = adventure; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " delete "); + NpcEntryImpl entry = context.parse(NpcEntryImpl.class); + npcRegistry.delete(entry.getId()); + adventure.sender(context.getSender()).sendMessage(Component.text("Deleted NPC with ID: " + entry.getId(), NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/ListCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ListCommand.java new file mode 100644 index 0000000..09bff36 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ListCommand.java @@ -0,0 +1,38 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; + +public class ListCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public ListCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + Component component = Component.text("Npc List:", NamedTextColor.GOLD).appendNewline(); + for (String id : npcRegistry.getModifiableIds()) { + NpcImpl npc = npcRegistry.getById(id).getNpc(); + NpcLocation location = npc.getLocation(); + component = component.append(Component.text("ID: " + id, npc.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Type: ", NamedTextColor.GREEN)) + .append(Component.text(npc.getType().getName(), NamedTextColor.GREEN)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Location: " + npc.getWorldName() + " X:" + location.getBlockX() + " Y:" + location.getBlockY() + " Z:" + location.getBlockZ(), NamedTextColor.GREEN)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("[TELEPORT]", NamedTextColor.DARK_GREEN).clickEvent(ClickEvent.runCommand("/znpcs teleport " + id))) + .appendNewline(); + } + context.send(component); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/LookAtMeCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/LookAtMeCommand.java new file mode 100644 index 0000000..d95a146 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/LookAtMeCommand.java @@ -0,0 +1,37 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; + +public class LookAtMeCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public LookAtMeCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " lookatme "); + Player player = context.ensureSenderIsPlayer(); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + npc.setLocation(npc.getLocation().lookingAt(player.getLocation())); + context.send(Component.text("NPC is now looking at you.", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/MoveCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/MoveCommand.java new file mode 100644 index 0000000..452cf44 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/MoveCommand.java @@ -0,0 +1,40 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class MoveCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public MoveCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " move "); + Player player = context.ensureSenderIsPlayer(); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + npc.setLocation(new NpcLocation(player.getLocation())); + if (!Objects.equals(npc.getWorld(), player.getWorld())) npc.setWorld(player.getWorld()); + context.send(Component.text("NPC moved to your current location.", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/NearCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/NearCommand.java new file mode 100644 index 0000000..6f965f7 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/NearCommand.java @@ -0,0 +1,60 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class NearCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public NearCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " near "); + Player player = context.ensureSenderIsPlayer(); + int raw = context.parse(Integer.class); + double radius = Math.pow(raw, 2); + + List entries = npcRegistry.getAllModifiable().stream() + .filter(entry -> Objects.equals(entry.getNpc().getWorld(), player.getWorld())) + .filter(entry -> { + Location loc = entry.getNpc().getBukkitLocation(); + return loc != null && loc.distanceSquared(player.getLocation()) < radius; + }) + .collect(Collectors.toList()); + + if (entries.isEmpty()) context.halt(Component.text("There are no npcs within " + raw + " blocks around you.", NamedTextColor.RED)); + + Component component = Component.text("All NPCs that are within " + raw + " blocks from you:", NamedTextColor.GOLD).appendNewline(); + for (NpcEntryImpl entry : entries) { + NpcImpl npc = entry.getNpc(); + NpcLocation location = npc.getLocation(); + component = component.append(Component.text("ID: " + entry.getId(), npc.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Type: ", NamedTextColor.GREEN)) + .append(Component.text(npc.getType().getName(), NamedTextColor.GREEN)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Location: " + npc.getWorldName() + " X:" + location.getBlockX() + " Y:" + location.getBlockY() + " Z:" + location.getBlockZ(), NamedTextColor.GREEN)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("[TELEPORT]", NamedTextColor.DARK_GREEN).clickEvent(ClickEvent.runCommand("/znpcs teleport " + entry.getId()))) + .appendNewline(); + } + context.send(component); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/ReloadConfigCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ReloadConfigCommand.java new file mode 100644 index 0000000..2b9e12a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ReloadConfigCommand.java @@ -0,0 +1,22 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.config.ConfigManager; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +public class ReloadConfigCommand implements CommandHandler { + private final ConfigManager configManager; + + public ReloadConfigCommand(ConfigManager configManager) { + this.configManager = configManager; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + configManager.reload(); + context.send(Component.text("Plugin configuration reloaded successfully", NamedTextColor.GREEN)); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/SetLocationCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/SetLocationCommand.java new file mode 100644 index 0000000..948d017 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/SetLocationCommand.java @@ -0,0 +1,56 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class SetLocationCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public SetLocationCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " setlocation "); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + double x = parseLocation(context.popString(), npc.getLocation().getX()); + double y = parseLocation(context.popString(), npc.getLocation().getY()); + double z = parseLocation(context.popString(), npc.getLocation().getZ()); + npc.setLocation(new NpcLocation(x, y, z, npc.getLocation().getYaw(), npc.getLocation().getPitch())); + context.send(Component.text("NPC has been moved to " + x + ", " + y + ", " + z + ".", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + NpcImpl npc = context.suggestionParse(0, NpcEntryImpl.class).getNpc(); + if (context.argSize() == 2) return Arrays.asList(String.valueOf(npc.getLocation().getX()), "~"); + else if (context.argSize() == 3) return Arrays.asList(String.valueOf(npc.getLocation().getY()), "~"); + else if (context.argSize() == 4) return Arrays.asList(String.valueOf(npc.getLocation().getZ()), "~"); + return Collections.emptyList(); + } + + private static double parseLocation(String input, double current) throws CommandExecutionException { + if (input.equals("~")) return current; + if (input.startsWith("~")) { + try { + return current + Double.parseDouble(input.substring(1)); + } catch (NumberFormatException e) { + throw new CommandExecutionException(); + } + } + return Double.parseDouble(input); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/SetRotationCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/SetRotationCommand.java new file mode 100644 index 0000000..dda74dc --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/SetRotationCommand.java @@ -0,0 +1,58 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class SetRotationCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public SetRotationCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " setrotation "); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + float yaw = parseRotation(context.popString(), npc.getLocation().getYaw()); + float pitch = parseRotation(context.popString(), npc.getLocation().getPitch()); + if (pitch < -90 || pitch > 90) { + pitch = Math.min(Math.max(pitch, -90), 90); + context.send(Component.text("Warning: pitch is outside of the -90 to 90 range. It has been normalized to " + pitch + ".", NamedTextColor.YELLOW)); + } + npc.setLocation(new NpcLocation(npc.getLocation().getX(), npc.getLocation().getY(), npc.getLocation().getZ(), yaw, pitch)); + context.send(Component.text("NPC has been rotated to " + yaw + ", " + pitch + ".", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + NpcImpl npc = context.suggestionParse(0, NpcEntryImpl.class).getNpc(); + if (context.argSize() == 2) return Arrays.asList(String.valueOf(npc.getLocation().getYaw()), "~"); + else if (context.argSize() == 3) return Arrays.asList(String.valueOf(npc.getLocation().getPitch()), "~"); + return Collections.emptyList(); + } + + private static float parseRotation(String input, float current) throws CommandExecutionException { + if (input.equals("~")) return current; + if (input.startsWith("~")) { + try { + return current + Float.parseFloat(input.substring(1)); + } catch (NumberFormatException e) { + throw new CommandExecutionException(); + } + } + return Float.parseFloat(input); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/SkinCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/SkinCommand.java new file mode 100644 index 0000000..48fa332 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/SkinCommand.java @@ -0,0 +1,143 @@ +package lol.pyr.znpcsplus.commands; + +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.skin.descriptor.NameFetchingDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.MirrorDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.io.File; +import java.io.FileNotFoundException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class SkinCommand implements CommandHandler { + private final MojangSkinCache skinCache; + private final NpcRegistryImpl npcRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + + public SkinCommand(MojangSkinCache skinCache, NpcRegistryImpl npcRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry) { + this.skinCache = skinCache; + this.npcRegistry = npcRegistry; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " skin [value]"); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + if (npc.getType() != typeRegistry.getByEntityType(EntityTypes.PLAYER)) context.halt(Component.text("The NPC must be a player to have a skin", NamedTextColor.RED)); + String type = context.popString(); + + if (type.equalsIgnoreCase("mirror")) { + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new MirrorDescriptor(skinCache)); + npc.respawn(); + context.halt(Component.text("The NPC's skin will now mirror the player that it's being displayed to", NamedTextColor.GREEN)); + } else if (type.equalsIgnoreCase("static")) { + context.ensureArgsNotEmpty(); + String name = context.dumpAllArgs(); + context.send(Component.text("Fetching skin \"" + name + "\"...", NamedTextColor.GREEN)); + PrefetchedDescriptor.forPlayer(skinCache, name).thenAccept(skin -> { + if (skin.getSkin() == null) { + context.send(Component.text("Failed to fetch skin, are you sure the player name is valid?", NamedTextColor.RED)); + return; + } + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), skin); + npc.respawn(); + context.send(Component.text("The NPC's skin has been set to \"" + name + "\"", NamedTextColor.GREEN)); + }); + return; + } else if (type.equalsIgnoreCase("dynamic")) { + context.ensureArgsNotEmpty(); + String name = context.dumpAllArgs(); + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new NameFetchingDescriptor(skinCache, name)); + npc.respawn(); + context.halt(Component.text("The NPC's skin will now be resolved per-player from \"" + name + "\"")); + } else if (type.equalsIgnoreCase("url")) { + context.ensureArgsNotEmpty(); + String variant = context.popString().toLowerCase(); + if (!variant.equalsIgnoreCase("slim") && !variant.equalsIgnoreCase("classic")) { + context.send(Component.text("Invalid skin variant! Please use one of the following: slim, classic", NamedTextColor.RED)); + return; + } + String urlString = context.dumpAllArgs(); + try { + URL url = new URL(urlString); + context.send(Component.text("Fetching skin from url \"" + urlString + "\"...", NamedTextColor.GREEN)); + PrefetchedDescriptor.fromUrl(skinCache, url , variant).thenAccept(skin -> { + if (skin.getSkin() == null) { + context.send(Component.text("Failed to fetch skin, are you sure the url is valid?", NamedTextColor.RED)); + return; + } + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), skin); + npc.respawn(); + context.send(Component.text("The NPC's skin has been set.", NamedTextColor.GREEN)); + }); + } catch (MalformedURLException e) { + context.send(Component.text("Invalid url!", NamedTextColor.RED)); + } + return; + } else if (type.equalsIgnoreCase("file")) { + context.ensureArgsNotEmpty(); + String path = context.dumpAllArgs(); + context.send(Component.text("Fetching skin from file \"" + path + "\"...", NamedTextColor.GREEN)); + PrefetchedDescriptor.fromFile(skinCache, path).exceptionally(e -> { + if (e instanceof FileNotFoundException || e.getCause() instanceof FileNotFoundException) { + context.send(Component.text("A file at the specified path could not be found!", NamedTextColor.RED)); + } else { + context.send(Component.text("An error occurred while fetching the skin from file! Check the console for more details.", NamedTextColor.RED)); + //noinspection CallToPrintStackTrace + e.printStackTrace(); + } + return null; + }).thenAccept(skin -> { + if (skin == null) return; + if (skin.getSkin() == null) { + context.send(Component.text("Failed to fetch skin, are you sure the file path is valid?", NamedTextColor.RED)); + return; + } + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), skin); + npc.respawn(); + context.send(Component.text("The NPC's skin has been set.", NamedTextColor.GREEN)); + }); + return; + } + context.send(Component.text("Unknown skin type! Please use one of the following: mirror, static, dynamic, url")); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestLiteral("mirror", "static", "dynamic", "url", "file"); + if (context.matchSuggestion("*", "static")) return context.suggestPlayers(); + if (context.argSize() == 3 && context.matchSuggestion("*", "url")) { + return context.suggestLiteral("slim", "classic"); + } + if (context.argSize() == 3 && context.matchSuggestion("*", "file")) { + if (skinCache.getSkinsFolder().exists()) { + File[] files = skinCache.getSkinsFolder().listFiles(); + if (files != null) { + return Arrays.stream(files).map(File::getName).collect(Collectors.toList()); + } + } + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/TeleportCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/TeleportCommand.java new file mode 100644 index 0000000..b9705cd --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/TeleportCommand.java @@ -0,0 +1,41 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.FoliaUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; + +public class TeleportCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public TeleportCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " teleport "); + Player player = context.ensureSenderIsPlayer(); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + Location location = npc.getBukkitLocation(); + if (location == null) context.halt("Unable to teleport to NPC, the world is not loaded!"); + FoliaUtil.teleport(player, location); + context.send(Component.text("Teleported to NPC!", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/ToggleCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ToggleCommand.java new file mode 100644 index 0000000..60a09c8 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ToggleCommand.java @@ -0,0 +1,42 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class ToggleCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public ToggleCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " toggle []"); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + boolean enabled; + if (context.argSize() == 1) { + enabled = context.popString().equalsIgnoreCase("enable"); + } else { + enabled = !npc.isEnabled(); + } + npc.setEnabled(enabled); + context.send(Component.text("NPC has been " + (enabled ? "enabled" : "disabled"), NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestLiteral("enable", "disable"); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/TypeCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/TypeCommand.java new file mode 100644 index 0000000..4d57468 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/TypeCommand.java @@ -0,0 +1,37 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.*; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class TypeCommand implements CommandHandler { + private final NpcRegistryImpl registry; + private final NpcTypeRegistryImpl typeRegistry; + + public TypeCommand(NpcRegistryImpl registry, NpcTypeRegistryImpl typeRegistry) { + this.registry = registry; + this.typeRegistry = typeRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " type "); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + NpcTypeImpl type = context.parse(NpcTypeImpl.class); + npc.setType(type); + context.send(Component.text("NPC type set to " + type.getName() + ".", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(registry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(typeRegistry.getAllImpl().stream().map(NpcTypeImpl::getName)); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/VersionCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/VersionCommand.java new file mode 100644 index 0000000..77ce2c0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/VersionCommand.java @@ -0,0 +1,66 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.ZNpcsPlus; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.jar.Attributes; +import java.util.jar.JarFile; + +public class VersionCommand implements CommandHandler { + + private final String pluginVersion; + private final String gitBranch; + private final String gitCommitHash; + private final String buildId; + + public VersionCommand(ZNpcsPlus plugin) { + pluginVersion = plugin.getDescription().getVersion(); + String gitBranch = ""; + String gitCommitHash = ""; + String buildId = ""; + try { + URL jarUrl = getClass().getProtectionDomain().getCodeSource().getLocation(); + JarFile jarFile = new JarFile(jarUrl.toURI().getPath()); + Attributes attributes = jarFile.getManifest().getMainAttributes(); + gitBranch = attributes.getValue("Git-Branch"); + gitCommitHash = attributes.getValue("Git-Commit-Hash"); + buildId = attributes.getValue("Build-Id"); + } catch (IOException | URISyntaxException e) { + e.printStackTrace(); + } + this.gitBranch = gitBranch; + this.gitCommitHash = gitCommitHash; + this.buildId = buildId; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + + StringBuilder versionBuilder = new StringBuilder("This server is running ZNPCsPlus version ").append(pluginVersion); + if (gitBranch != null && !gitBranch.isEmpty()) { + versionBuilder.append("-").append(gitBranch); + } + if (gitCommitHash != null && !gitCommitHash.isEmpty()) { + versionBuilder.append("@").append(gitCommitHash); + } + if (buildId != null && !buildId.isEmpty()) { + versionBuilder.append(" (Build #").append(buildId).append(")"); + } else { + versionBuilder.append(" (Development Build)"); + } + + String version = versionBuilder.toString(); + + context.send(Component.text(version, NamedTextColor.GREEN) + .hoverEvent(Component.text("Click to copy version to clipboard")) + .clickEvent(ClickEvent.copyToClipboard(version))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionAddCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionAddCommand.java new file mode 100644 index 0000000..09b0749 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionAddCommand.java @@ -0,0 +1,51 @@ +package lol.pyr.znpcsplus.commands.action; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.interaction.InteractionCommandHandler; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class ActionAddCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + private final ActionRegistryImpl actionRegistry; + + public ActionAddCommand(NpcRegistryImpl npcRegistry, ActionRegistryImpl actionRegistry) { + this.npcRegistry = npcRegistry; + this.actionRegistry = actionRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + List commands = actionRegistry.getCommands(); + context.setUsage(context.getLabel() + " action add "); + String sub = context.popString(); + for (InteractionCommandHandler command : commands) if (command.getSubcommandName().equalsIgnoreCase(sub)) { + context.setUsage(context.getLabel() + " action add"); + command.run(context); + return; + } + context.send(Component.text("Invalid action type, available action types:\n" + + commands.stream().map(InteractionCommandHandler::getSubcommandName).collect(Collectors.joining(", ")), NamedTextColor.RED)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + List commands = actionRegistry.getCommands(); + if (context.argSize() == 1) return context.suggestStream(commands.stream().map(InteractionCommandHandler::getSubcommandName)); + if (context.argSize() == 2) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() >= 3) { + String sub = context.popString(); + context.popString(); + for (InteractionCommandHandler command : commands) if (command.getSubcommandName().equalsIgnoreCase(sub)) return command.suggest(context); + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionClearCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionClearCommand.java new file mode 100644 index 0000000..2aa709a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionClearCommand.java @@ -0,0 +1,36 @@ +package lol.pyr.znpcsplus.commands.action; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class ActionClearCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public ActionClearCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " action clear "); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + if (npc.getActions().size() == 0) context.halt(Component.text("That npc doesn't have any actions", NamedTextColor.RED)); + npc.clearActions(); + context.send(Component.text("Removed all actions from the npc", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionDeleteCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionDeleteCommand.java new file mode 100644 index 0000000..b282a35 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionDeleteCommand.java @@ -0,0 +1,41 @@ +package lol.pyr.znpcsplus.commands.action; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public class ActionDeleteCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public ActionDeleteCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " action delete "); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + int index = context.parse(Integer.class); + if (index >= npc.getActions().size() || index < 0) context.halt(Component.text("That npc doesn't have any action with the index " + index, NamedTextColor.RED)); + npc.removeAction(index); + context.send(Component.text("Removed action with index " + index, NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(Stream.iterate(0, n -> n + 1) + .limit(context.suggestionParse(0, NpcEntryImpl.class).getNpc().getActions().size()) + .map(String::valueOf)); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionEditCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionEditCommand.java new file mode 100644 index 0000000..0b0eafe --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionEditCommand.java @@ -0,0 +1,64 @@ +package lol.pyr.znpcsplus.commands.action; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.interaction.InteractionCommandHandler; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ActionEditCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + private final ActionRegistryImpl actionRegistry; + + private InteractionCommandHandler commandHandler = null; + + public ActionEditCommand(NpcRegistryImpl npcRegistry, ActionRegistryImpl actionRegistry) { + this.npcRegistry = npcRegistry; + this.actionRegistry = actionRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " action edit ..."); + NpcEntryImpl entry = context.parse(NpcEntryImpl.class); + int index = context.parse(Integer.class); + if (index >= entry.getNpc().getActions().size() || index < 0) context.halt(Component.text("That npc doesn't have any action with the index " + index, NamedTextColor.RED)); + List commands = actionRegistry.getCommands(); + String sub = context.popString(); + for (InteractionCommandHandler command : commands) if (command.getSubcommandName().equalsIgnoreCase(sub)) { + this.commandHandler = command; + } + if (this.commandHandler == null) { + context.send(Component.text("Invalid action type, available action types:\n" + + commands.stream().map(InteractionCommandHandler::getSubcommandName).collect(Collectors.joining(", ")), NamedTextColor.RED)); + } + InteractionAction newAction = this.commandHandler.parse(context); + entry.getNpc().editAction(index, newAction); + context.send(Component.text("Edited action with index " + index + " of Npc " + entry.getId(), NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(Stream.iterate(0, n -> n + 1) + .limit(context.suggestionParse(0, NpcEntryImpl.class).getNpc().getActions().size()) + .map(String::valueOf)); + List commands = actionRegistry.getCommands(); + if (context.argSize() == 3) return context.suggestStream(commands.stream().map(InteractionCommandHandler::getSubcommandName)); + context.popString(); + context.popString(); + String sub = context.popString(); + for (InteractionCommandHandler command : commands) if (command.getSubcommandName().equalsIgnoreCase(sub)) return command.suggest(context); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionListCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionListCommand.java new file mode 100644 index 0000000..fe12bd9 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionListCommand.java @@ -0,0 +1,39 @@ +package lol.pyr.znpcsplus.commands.action; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; + +import java.util.Collections; +import java.util.List; + +public class ActionListCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public ActionListCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " action list "); + NpcEntryImpl entry = context.parse(NpcEntryImpl.class); + List actions = entry.getNpc().getActions(); + context.send("Actions of Npc " + entry.getId() + ":"); + for (int i = 0; i < actions.size(); i++) { + if (actions.get(i) instanceof InteractionActionImpl) { + context.send(((InteractionActionImpl) actions.get(i)).getInfo(entry.getId(), i, context)); + } + } + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloAddCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloAddCommand.java new file mode 100644 index 0000000..f739d18 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloAddCommand.java @@ -0,0 +1,43 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.hologram.HologramItem; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class HoloAddCommand implements CommandHandler { + private final NpcRegistryImpl registry; + + public HoloAddCommand(NpcRegistryImpl registry) { + this.registry = registry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo add "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + context.ensureArgsNotEmpty(); + String in = context.dumpAllArgs(); + if (in.toLowerCase().startsWith("item:")) { + if (!HologramItem.ensureValidItemInput(in.substring(5))) { + context.halt(Component.text("The item input is invalid!", NamedTextColor.RED)); + } + } + hologram.addLine(in); + context.send(Component.text("NPC line added!", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(registry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloAddItemCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloAddItemCommand.java new file mode 100644 index 0000000..cb23a56 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloAddItemCommand.java @@ -0,0 +1,39 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; + +public class HoloAddItemCommand implements CommandHandler { + private final NpcRegistryImpl registry; + + public HoloAddItemCommand(NpcRegistryImpl registry) { + this.registry = registry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo additem "); + Player player = context.ensureSenderIsPlayer(); + org.bukkit.inventory.ItemStack itemStack = player.getInventory().getItemInHand(); + if (itemStack == null) context.halt(Component.text("You must be holding an item!", NamedTextColor.RED)); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + hologram.addItemLineStack(itemStack); + context.send(Component.text("NPC item line added!", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(registry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloDeleteCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloDeleteCommand.java new file mode 100644 index 0000000..fdf5b27 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloDeleteCommand.java @@ -0,0 +1,41 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public class HoloDeleteCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloDeleteCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo delete "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + int line = context.parse(Integer.class); + if (line < 0 || line >= hologram.getLines().size()) context.halt(Component.text("Invalid line number!", NamedTextColor.RED)); + hologram.removeLine(line); + context.send(Component.text("NPC line removed.", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(Stream.iterate(0, n -> n + 1) + .limit(context.suggestionParse(0, NpcEntryImpl.class).getNpc().getHologram().getLines().size()) + .map(String::valueOf)); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInfoCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInfoCommand.java new file mode 100644 index 0000000..20c5a6c --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInfoCommand.java @@ -0,0 +1,41 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class HoloInfoCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloInfoCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo info "); + NpcEntryImpl entry = context.parse(NpcEntryImpl.class); + HologramImpl hologram = entry.getNpc().getHologram(); + Component component = Component.text("NPC Hologram Info of ID " + entry.getId() + ":", NamedTextColor.GREEN).appendNewline(); + for (int i = 0; i < hologram.getLines().size(); i++) { + component = component.append(Component.text(i + ") ", NamedTextColor.GREEN)) + .append(Component.text(hologram.getLine(i), NamedTextColor.WHITE)) + .appendNewline(); + } + context.send(component); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInsertCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInsertCommand.java new file mode 100644 index 0000000..8bb8471 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInsertCommand.java @@ -0,0 +1,49 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.hologram.HologramItem; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public class HoloInsertCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloInsertCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo insert "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + int line = context.parse(Integer.class); + if (line < 0 || line >= hologram.getLines().size()) context.halt(Component.text("Invalid line number!", NamedTextColor.RED)); + context.ensureArgsNotEmpty(); + String in = context.dumpAllArgs(); + if (in.toLowerCase().startsWith("item:")) { + if (!HologramItem.ensureValidItemInput(in.substring(5))) { + context.halt(Component.text("The item input is invalid!", NamedTextColor.RED)); + } + } + hologram.insertLine(line, in); + context.send(Component.text("NPC line inserted!", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(Stream.iterate(0, n -> n + 1) + .limit(context.suggestionParse(0, NpcEntryImpl.class).getNpc().getHologram().getLines().size()) + .map(String::valueOf)); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInsertItemCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInsertItemCommand.java new file mode 100644 index 0000000..68273e4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInsertItemCommand.java @@ -0,0 +1,45 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public class HoloInsertItemCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloInsertItemCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo insertitem "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + int line = context.parse(Integer.class); + if (line < 0 || line >= hologram.getLines().size()) context.halt(Component.text("Invalid line number!", NamedTextColor.RED)); + Player player = context.ensureSenderIsPlayer(); + org.bukkit.inventory.ItemStack itemStack = player.getInventory().getItemInHand(); + if (itemStack == null) context.halt(Component.text("You must be holding an item!", NamedTextColor.RED)); + hologram.insertItemLineStack(line, itemStack); + context.send(Component.text("NPC item line inserted!", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(Stream.iterate(0, n -> n + 1) + .limit(context.suggestionParse(0, NpcEntryImpl.class).getNpc().getHologram().getLines().size()) + .map(String::valueOf)); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloOffsetCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloOffsetCommand.java new file mode 100644 index 0000000..21f9163 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloOffsetCommand.java @@ -0,0 +1,38 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; + +import java.util.Collections; +import java.util.List; + +public class HoloOffsetCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloOffsetCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo offset "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + double offset = context.parse(Double.class); + hologram.setOffset(offset); + context.send("NPC hologram offset set!"); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) { + HologramImpl hologram = context.suggestionParse(0, NpcEntryImpl.class).getNpc().getHologram(); + return context.suggestLiteral(String.valueOf(hologram.getOffset())); + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloRefreshDelayCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloRefreshDelayCommand.java new file mode 100644 index 0000000..bdf3120 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloRefreshDelayCommand.java @@ -0,0 +1,38 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; + +import java.util.Collections; +import java.util.List; + +public class HoloRefreshDelayCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloRefreshDelayCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo refreshdelay "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + double delay = context.parse(Double.class); + hologram.setRefreshDelay((long) (delay * 1000)); + context.send("NPC refresh delay set!"); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) { + HologramImpl hologram = context.suggestionParse(0, NpcEntryImpl.class).getNpc().getHologram(); + return context.suggestLiteral(String.valueOf(((double) hologram.getRefreshDelay()) / 1000)); + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloSetCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloSetCommand.java new file mode 100644 index 0000000..7b106b0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloSetCommand.java @@ -0,0 +1,46 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public class HoloSetCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloSetCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo set "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + int line = context.parse(Integer.class); + if (line < 0 || line >= hologram.getLines().size()) context.halt(Component.text("Invalid line number!", NamedTextColor.RED)); + context.ensureArgsNotEmpty(); + hologram.removeLine(line); + hologram.insertLine(line, context.dumpAllArgs()); + context.send(Component.text("NPC line set!", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() >= 2) { + HologramImpl hologram = context.suggestionParse(0, NpcEntryImpl.class).getNpc().getHologram(); + if (context.argSize() == 2) return context.suggestStream(Stream.iterate(0, n -> n + 1) + .limit(hologram.getLines().size()).map(String::valueOf)); + if (context.argSize() == 3) return context.suggestLiteral(hologram.getLine(context.suggestionParse(1, Integer.class))); + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloSetItemCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloSetItemCommand.java new file mode 100644 index 0000000..1e623ab --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloSetItemCommand.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public class HoloSetItemCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloSetItemCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo setitem "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + int line = context.parse(Integer.class); + if (line < 0 || line >= hologram.getLines().size()) context.halt(Component.text("Invalid line number!", NamedTextColor.RED)); + Player player = context.ensureSenderIsPlayer(); + org.bukkit.inventory.ItemStack itemStack = player.getInventory().getItemInHand(); + if (itemStack == null) context.halt(Component.text("You must be holding an item!", NamedTextColor.RED)); + hologram.removeLine(line); + hologram.insertItemLineStack(line, itemStack); + context.send(Component.text("NPC item line set!", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() >= 2) { + HologramImpl hologram = context.suggestionParse(0, NpcEntryImpl.class).getNpc().getHologram(); + if (context.argSize() == 2) return context.suggestStream(Stream.iterate(0, n -> n + 1) + .limit(hologram.getLines().size()).map(String::valueOf)); + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertyRemoveCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertyRemoveCommand.java new file mode 100644 index 0000000..88b6ff1 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertyRemoveCommand.java @@ -0,0 +1,43 @@ +package lol.pyr.znpcsplus.commands.property; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class PropertyRemoveCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public PropertyRemoveCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " property remove "); + NpcEntryImpl entry = context.parse(NpcEntryImpl.class); + NpcImpl npc = entry.getNpc(); + EntityPropertyImpl property = context.parse(EntityPropertyImpl.class); + if (!npc.hasProperty(property)) context.halt(Component.text("This npc doesn't have the " + property.getName() + " property set", NamedTextColor.RED)); + if (!property.isPlayerModifiable()) context.halt(Component.text("This property is not modifiable by players", NamedTextColor.RED)); + npc.setProperty(property, null); + context.send(Component.text("Removed property " + property.getName() + " from NPC " + entry.getId(), NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(context.suggestionParse(0, NpcEntryImpl.class) + .getNpc().getAllProperties().stream().filter(EntityProperty::isPlayerModifiable).map(EntityProperty::getName)); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertySetCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertySetCommand.java new file mode 100644 index 0000000..3d9b228 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertySetCommand.java @@ -0,0 +1,216 @@ +package lol.pyr.znpcsplus.commands.property; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState; +import com.github.retrooper.packetevents.protocol.world.states.type.StateType; +import com.github.retrooper.packetevents.protocol.world.states.type.StateTypes; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.properties.attributes.AttributeProperty; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.*; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Color; +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class PropertySetCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public PropertySetCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " property set "); + NpcEntryImpl entry = context.parse(NpcEntryImpl.class); + NpcImpl npc = entry.getNpc(); + EntityPropertyImpl property = context.parse(EntityPropertyImpl.class); + + // TODO: find a way to do this better & rewrite this mess + + if (!npc.getType().getAllowedProperties().contains(property)) context.halt(Component.text("Property " + property.getName() + " not allowed for npc type " + npc.getType().getName(), NamedTextColor.RED)); + if (!property.isPlayerModifiable()) context.halt(Component.text("This property is not modifiable by players", NamedTextColor.RED)); + Class type = property.getType(); + Object value; + String valueName; + if (type == ItemStack.class) { + org.bukkit.inventory.ItemStack bukkitStack = context.ensureSenderIsPlayer().getInventory().getItemInHand(); + if (bukkitStack.getAmount() == 0) { + value = null; + valueName = "EMPTY"; + } else { + value = SpigotConversionUtil.fromBukkitItemStack(bukkitStack); + valueName = bukkitStack.toString(); + } + } + else if (type == NamedColor.class && context.argSize() < 1 && npc.getProperty(property) != null) { + value = null; + valueName = "NONE"; + } + else if (type == Color.class && context.argSize() < 1 && npc.getProperty(property) != null) { + value = Color.BLACK; + valueName = "NONE"; + } + else if (type == ParrotVariant.class && context.argSize() < 1 && npc.getProperty(property) != null) { + value = null; + valueName = "NONE"; + } + else if (type == BlockState.class) { + String inputType = context.popString().toLowerCase(); + switch (inputType) { + case "hand": + org.bukkit.inventory.ItemStack bukkitStack = context.ensureSenderIsPlayer().getInventory().getItemInHand(); + if (bukkitStack.getAmount() == 0) { + value = new BlockState(0); + valueName = "EMPTY"; + } else { + WrappedBlockState blockState = StateTypes.getByName(bukkitStack.getType().name().toLowerCase()).createBlockState(); +// WrappedBlockState blockState = WrappedBlockState.getByString(bukkitStack.getType().name().toLowerCase()); + value = new BlockState(blockState.getGlobalId()); + valueName = bukkitStack.toString(); + } + break; + case "looking_at": + + // TODO + + value = new BlockState(0); + valueName = "EMPTY"; + break; + case "block": + context.ensureArgsNotEmpty(); + WrappedBlockState blockState = WrappedBlockState.getByString(context.popString()); + value = new BlockState(blockState.getGlobalId()); + valueName = blockState.toString(); + break; + default: + context.send(Component.text("Invalid input type " + inputType + ", must be hand, looking_at, or block", NamedTextColor.RED)); + return; + } + } + else if (type == SpellType.class) { + if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_13)) { + value = context.parse(type); + valueName = String.valueOf(value); + if (((SpellType) value).ordinal() > 3) { + context.send(Component.text("Spell type " + valueName + " is not supported on this version", NamedTextColor.RED)); + return; + } + } + else { + value = context.parse(type); + valueName = String.valueOf(value); + } + } + else if (type == NpcEntryImpl.class) { + value = context.parse(type); + valueName = value == null ? "NONE" : ((NpcEntryImpl) value).getId(); + } + else if (type == Vector3i.class) { + value = context.parse(type); + valueName = value == null ? "NONE" : ((Vector3i) value).toPrettyString(); + } + else if (property instanceof AttributeProperty) { + value = context.parse(type); + if ((double) value < ((AttributeProperty) property).getMinValue() || (double) value > ((AttributeProperty) property).getMaxValue()) { + double sanitizedValue = ((AttributeProperty) property).sanitizeValue((double) value); + context.send(Component.text("WARNING: Value " + value + " is out of range for property " + property.getName() + ", setting to " + sanitizedValue, NamedTextColor.YELLOW)); + value = sanitizedValue; + } + valueName = String.valueOf(value); + } + else { + try { + value = context.parse(type); + valueName = String.valueOf(value); + } catch (NullPointerException e) { + context.send(Component.text("An error occurred while trying to parse the value. Please report this to the plugin author.", + NamedTextColor.RED)); + e.printStackTrace(); + return; + } + } + + npc.UNSAFE_setProperty(property, value); + context.send(Component.text("Set property " + property.getName() + " for NPC " + entry.getId() + " to " + valueName, NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(context.suggestionParse(0, NpcEntryImpl.class) + .getNpc().getType().getAllowedProperties().stream().map(EntityProperty::getName)); + if (context.argSize() >= 3) { + EntityPropertyImpl property = context.suggestionParse(1, EntityPropertyImpl.class); + Class type = property.getType(); + if (type == Vector3f.class && context.argSize() <= 5) return context.suggestLiteral("0", "0.0"); + if (context.argSize() == 3) { + if (type == Boolean.class) return context.suggestLiteral("true", "false"); + if (type == NamedColor.class) return context.suggestEnum(NamedColor.values()); + if (type == Color.class) return context.suggestLiteral("0x0F00FF", "#FFFFFF"); + if (type == BlockState.class) return context.suggestLiteral("hand", "looking_at", "block"); + if (type == SpellType.class) return PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_13) ? + context.suggestEnum(Arrays.stream(SpellType.values()).filter(spellType -> spellType.ordinal() <= 3).toArray(SpellType[]::new)) : + context.suggestEnum(SpellType.values()); + + if (type == Vector3i.class) { + if (context.getSender() instanceof Player) { + Player player = (Player) context.getSender(); + Block targetBlock = player.getTargetBlock(Collections.singleton(Material.AIR), 5); + if (targetBlock.getType().equals(Material.AIR)) return Collections.emptyList(); + return context.suggestLiteral( + targetBlock.getX() + "", + targetBlock.getX() + " " + targetBlock.getY(), + targetBlock.getX() + " " + targetBlock.getY() + " " + targetBlock.getZ()); + } + } + // Suggest enum values directly + if (type.isEnum()) { + return context.suggestEnum((Enum[]) type.getEnumConstants()); + } + } + else if (context.argSize() == 4) { + if (type == BlockState.class) { + // TODO: suggest block with nbt like minecraft setblock command + return context.suggestionParse(2, String.class).equals("block") ? context.suggestStream(StateTypes.values().stream().map(StateType::getName)) : Collections.emptyList(); + } + if (type == Vector3i.class) { + if (context.getSender() instanceof Player) { + Player player = (Player) context.getSender(); + Block targetBlock = player.getTargetBlock(Collections.singleton(Material.AIR), 5); + if (targetBlock.getType().equals(Material.AIR)) return Collections.emptyList(); + return context.suggestLiteral( + targetBlock.getY() + "", + targetBlock.getY() + " " + targetBlock.getZ()); + } + } + } else if (context.argSize() == 5) { + if (type == Vector3i.class) { + if (context.getSender() instanceof Player) { + Player player = (Player) context.getSender(); + Block targetBlock = player.getTargetBlock(Collections.singleton(Material.AIR), 5); + if (targetBlock.getType().equals(Material.AIR)) return Collections.emptyList(); + return context.suggestLiteral(targetBlock.getZ() + ""); + } + } + } + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/ImportCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/ImportCommand.java new file mode 100644 index 0000000..c6c598e --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/ImportCommand.java @@ -0,0 +1,57 @@ +package lol.pyr.znpcsplus.commands.storage; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.conversion.DataImporter; +import lol.pyr.znpcsplus.conversion.DataImporterRegistry; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.FutureUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class ImportCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + private final DataImporterRegistry importerRegistry; + + public ImportCommand(NpcRegistryImpl npcRegistry, DataImporterRegistry importerRegistry) { + this.npcRegistry = npcRegistry; + this.importerRegistry = importerRegistry; + } + + @SuppressWarnings("ConstantConditions") + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " storage import "); + String id = context.popString().toUpperCase(); + DataImporter importer = importerRegistry.getImporter(id); + if (importer == null) context.halt(Component.text("Importer not found! Possible importers: " + + String.join(", ", importerRegistry.getIds()), NamedTextColor.RED)); + + FutureUtil.exceptionPrintingRunAsync(() -> { + if (!importer.isValid()) { + context.send(Component.text("There is no data to import from this importer!", NamedTextColor.RED)); + return; + } + try { + Collection entries = importer.importData(); + npcRegistry.registerAll(entries); + context.send(Component.text(entries.size() + " npcs have been loaded from " + id, NamedTextColor.GREEN)); + } catch (Exception exception) { + context.send(Component.text("Importing failed! Please check the console for more details.", NamedTextColor.RED)); + exception.printStackTrace(); + } + }); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(importerRegistry.getIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/LoadAllCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/LoadAllCommand.java new file mode 100644 index 0000000..93cc3de --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/LoadAllCommand.java @@ -0,0 +1,33 @@ +package lol.pyr.znpcsplus.commands.storage; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.FutureUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class LoadAllCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public LoadAllCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + FutureUtil.exceptionPrintingRunAsync(() -> { + npcRegistry.reload(); + context.send(Component.text("All NPCs have been re-loaded from storage", NamedTextColor.GREEN)); + }); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/MigrateCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/MigrateCommand.java new file mode 100644 index 0000000..063b98a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/MigrateCommand.java @@ -0,0 +1,179 @@ +package lol.pyr.znpcsplus.commands.storage; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.ZNpcsPlus; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.serialization.NpcSerializerRegistryImpl; +import lol.pyr.znpcsplus.storage.NpcStorage; +import lol.pyr.znpcsplus.storage.NpcStorageType; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +public class MigrateCommand implements CommandHandler { + private final ConfigManager configManager; + private final ZNpcsPlus plugin; + private final PacketFactory packetFactory; + private final ActionRegistryImpl actionRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final LegacyComponentSerializer textSerializer; + private final NpcStorage currentStorage; + private final NpcStorageType currentStorageType; + private final NpcRegistryImpl npcRegistry; + private final NpcSerializerRegistryImpl serializerRegistry; + + public MigrateCommand(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, NpcStorage currentStorage, NpcStorageType currentStorageType, NpcRegistryImpl npcRegistry, NpcSerializerRegistryImpl serializerRegistry) { + this.configManager = configManager; + this.plugin = plugin; + this.packetFactory = packetFactory; + this.actionRegistry = actionRegistry; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.textSerializer = textSerializer; + this.currentStorage = currentStorage; + this.currentStorageType = currentStorageType; + this.npcRegistry = npcRegistry; + this.serializerRegistry = serializerRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " storage migrate [force]"); + NpcStorageType from = context.parse(NpcStorageType.class); + NpcStorageType to = context.parse(NpcStorageType.class); + boolean force = context.argSize() > 2 && context.parse(Boolean.class); + if (from.equals(to)) { + context.halt(Component.text("The storage types must be different.", NamedTextColor.RED)); + return; + } + NpcStorage fromStorage; + if (currentStorageType == from) { + fromStorage = currentStorage; + } else { + fromStorage = from.create(configManager, plugin, packetFactory, actionRegistry, typeRegistry, propertyRegistry, textSerializer, serializerRegistry); + if (fromStorage == null) { + context.halt(Component.text("Failed to initialize the source storage. Please check the console for more information.", NamedTextColor.RED)); + return; + } + } + Collection entries; + try { + entries = fromStorage.loadNpcs(); + } catch (Exception e) { + context.halt(Component.text("Failed to load NPCs from the source storage.", NamedTextColor.RED)); + return; + } + if (entries.isEmpty()) { + context.send(Component.text("No NPCs to migrate.", NamedTextColor.YELLOW)); + return; + } + NpcStorage toStorage; + if (currentStorageType == to) { + toStorage = currentStorage; + } else { + toStorage = to.create(configManager, plugin, packetFactory, actionRegistry, typeRegistry, propertyRegistry, textSerializer, serializerRegistry); + if (toStorage == null) { + context.halt(Component.text("Failed to initialize the destination storage. Please check the console for more information.", NamedTextColor.RED)); + return; + } + } + + Collection existingEntries; + try { + existingEntries = toStorage.loadNpcs(); + } catch (Exception e) { + context.halt(Component.text("Failed to load NPCs from the destination storage.", NamedTextColor.RED)); + return; + } + if (existingEntries.isEmpty()) { + toStorage.saveNpcs(entries); + context.send(Component.text("Migrated " + entries.size() + " NPCs from the source storage (", NamedTextColor.GREEN) + .append(Component.text(from.name(), NamedTextColor.GOLD)) + .append(Component.text(") to the destination storage (", NamedTextColor.GREEN)) + .append(Component.text(to.name(), NamedTextColor.GOLD)) + .append(Component.text(").", NamedTextColor.GREEN))); + if (currentStorageType == to) { + npcRegistry.reload(); + } else { + toStorage.close(); + } + return; + } + if (!force) { + Collection toSave = entries.stream().filter(e -> existingEntries.stream().noneMatch(e2 -> e2.getId().equals(e.getId()))).collect(Collectors.toList()); + Collection idExists = entries.stream().filter(e -> existingEntries.stream().anyMatch(e2 -> e2.getId().equals(e.getId()))).collect(Collectors.toList()); + if (toSave.isEmpty()) { + context.send(Component.text("No NPCs to migrate.", NamedTextColor.YELLOW)); + if (currentStorageType != to) { + toStorage.close(); + } + } else { + toStorage.saveNpcs(toSave); + context.send(Component.text("Migrated " + toSave.size() + " NPCs from the source storage (", NamedTextColor.GREEN) + .append(Component.text(from.name(), NamedTextColor.GOLD)) + .append(Component.text(") to the destination storage (", NamedTextColor.GREEN)) + .append(Component.text(to.name(), NamedTextColor.GOLD)) + .append(Component.text(").", NamedTextColor.GREEN))); + if (currentStorageType == to) { + npcRegistry.reload(); + } else { + toStorage.close(); + } + } + if (!idExists.isEmpty()) { + AtomicReference component = new AtomicReference<>(Component.text("The following NPCs were not migrated because their IDs already exist in the destination storage:").color(NamedTextColor.YELLOW)); + idExists.forEach(e -> { + component.set(component.get().append(Component.newline()).append(Component.text(e.getId(), NamedTextColor.RED))); + }); + component.set(component.get().append(Component.newline()) + .append(Component.text("Use the ", NamedTextColor.YELLOW)) + .append(Component.text("force", NamedTextColor.GOLD)) + .append(Component.text(" argument to overwrite them.", NamedTextColor.YELLOW))); + context.send(component.get()); + } + } else { + toStorage.saveNpcs(entries); + context.send(Component.text("Force migrated " + entries.size() + " NPCs from the source storage (", NamedTextColor.GREEN) + .append(Component.text(from.name(), NamedTextColor.GOLD)) + .append(Component.text(") to the destination storage (", NamedTextColor.GREEN)) + .append(Component.text(to.name(), NamedTextColor.GOLD)) + .append(Component.text(").", NamedTextColor.GREEN))); + if (currentStorageType == to) { + npcRegistry.reload(); + } else { + toStorage.close(); + } + } + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) { + return context.suggestEnum(NpcStorageType.values()); + } else if (context.argSize() == 2) { + NpcStorageType from = context.suggestionParse(0, NpcStorageType.class); + if (from == null) return Collections.emptyList(); + return context.suggestCollection(Arrays.stream(NpcStorageType.values()) + .filter(t -> t != from).map(Enum::name).collect(Collectors.toList())); + } else if (context.argSize() == 3) { + return context.suggestLiteral("true"); + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/SaveAllCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/SaveAllCommand.java new file mode 100644 index 0000000..4445cf1 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/SaveAllCommand.java @@ -0,0 +1,33 @@ +package lol.pyr.znpcsplus.commands.storage; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.FutureUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class SaveAllCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public SaveAllCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + FutureUtil.exceptionPrintingRunAsync(() -> { + npcRegistry.save(); + context.send(Component.text("All NPCs have been saved to storage", NamedTextColor.GREEN)); + }); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/config/ComponentSerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/config/ComponentSerializer.java new file mode 100644 index 0000000..a63beef --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/config/ComponentSerializer.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.config; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import space.arim.dazzleconf.error.BadValueException; +import space.arim.dazzleconf.serialiser.Decomposer; +import space.arim.dazzleconf.serialiser.FlexibleType; +import space.arim.dazzleconf.serialiser.ValueSerialiser; + +public class ComponentSerializer implements ValueSerialiser { + @Override + public Class getTargetClass() { + return Component.class; + } + + @Override + public Component deserialise(FlexibleType flexibleType) throws BadValueException { + return MiniMessage.miniMessage().deserialize(flexibleType.getString()); + } + + @Override + public Object serialise(Component value, Decomposer decomposer) { + return MiniMessage.miniMessage().serialize(value); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/config/ConfigManager.java b/plugin/src/main/java/lol/pyr/znpcsplus/config/ConfigManager.java new file mode 100644 index 0000000..30f35c5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/config/ConfigManager.java @@ -0,0 +1,63 @@ +package lol.pyr.znpcsplus.config; + +import space.arim.dazzleconf.ConfigurationFactory; +import space.arim.dazzleconf.ConfigurationOptions; +import space.arim.dazzleconf.error.ConfigFormatSyntaxException; +import space.arim.dazzleconf.error.InvalidConfigException; +import space.arim.dazzleconf.ext.snakeyaml.CommentMode; +import space.arim.dazzleconf.ext.snakeyaml.SnakeYamlConfigurationFactory; +import space.arim.dazzleconf.ext.snakeyaml.SnakeYamlOptions; +import space.arim.dazzleconf.helper.ConfigurationHelper; +import space.arim.dazzleconf.serialiser.ValueSerialiser; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Logger; + +public class ConfigManager { + private final static Logger logger = Logger.getLogger("ZNPCsPlus Configuration Manager"); + + private volatile MainConfig config; + private final ConfigurationHelper configHelper; + + private volatile MessageConfig messages; + private final ConfigurationHelper messagesHelper; + + public ConfigManager(File pluginFolder) { + configHelper = createHelper(MainConfig.class, new File(pluginFolder, "config.yaml")); + messagesHelper = createHelper(MessageConfig.class, new File(pluginFolder, "messages.yaml"), new ComponentSerializer()); + reload(); + } + + private static ConfigurationHelper createHelper(Class configClass, File file, ValueSerialiser... serialisers) { + SnakeYamlOptions yamlOptions = new SnakeYamlOptions.Builder().commentMode(CommentMode.fullComments()).build(); + ConfigurationOptions.Builder optionBuilder = new ConfigurationOptions.Builder(); + if (serialisers != null && serialisers.length > 0) optionBuilder.addSerialisers(serialisers); + ConfigurationFactory configFactory = SnakeYamlConfigurationFactory.create(configClass, optionBuilder.build(), yamlOptions); + return new ConfigurationHelper<>(file.getParentFile().toPath(), file.getName(), configFactory); + } + + public void reload() { + try { + config = configHelper.reloadConfigData(); + messages = messagesHelper.reloadConfigData(); + } catch (IOException e) { + logger.severe("Couldn't open config file!"); + e.printStackTrace(); + } catch (ConfigFormatSyntaxException e) { + logger.severe("Invalid config syntax!"); + e.printStackTrace(); + } catch (InvalidConfigException e) { + logger.severe("Invalid config value!"); + e.printStackTrace(); + } + } + + public MainConfig getConfig() { + return config; + } + + public MessageConfig getMessages() { + return messages; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/config/DatabaseConfig.java b/plugin/src/main/java/lol/pyr/znpcsplus/config/DatabaseConfig.java new file mode 100644 index 0000000..a62e983 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/config/DatabaseConfig.java @@ -0,0 +1,46 @@ +package lol.pyr.znpcsplus.config; + +import space.arim.dazzleconf.annote.ConfComments; +import space.arim.dazzleconf.annote.ConfDefault.*; +import space.arim.dazzleconf.annote.ConfKey; + +public interface DatabaseConfig { + @ConfKey("host") + @ConfComments("The host of the database") + @DefaultString("localhost") + String host(); + + @ConfKey("port") + @ConfComments("The port of the database") + @DefaultInteger(3306) + int port(); + + @ConfKey("username") + @ConfComments("The username to use to connect to the database") + @DefaultString("znpcsplus") + String username(); + + @ConfKey("password") + @ConfComments("The password to use to connect to the database") + @DefaultString("password") + String password(); + + @ConfKey("database-name") + @ConfComments("The name of the database to use") + @DefaultString("znpcsplus") + String databaseName(); + + @ConfKey("use-ssl") + @ConfComments("Should SSL be used when connecting to the database?") + @DefaultBoolean(false) + boolean useSSL(); + + default String createConnectionURL(String dbType) { + if (dbType.equalsIgnoreCase("mysql")) { + return "jdbc:mysql://" + host() + ":" + port() + "/" + databaseName() + "?useSSL=" + useSSL(); + } else { + throw new IllegalArgumentException("Unsupported database type: " + dbType); + } + } +} + diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/config/MainConfig.java b/plugin/src/main/java/lol/pyr/znpcsplus/config/MainConfig.java new file mode 100644 index 0000000..fee1ea7 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/config/MainConfig.java @@ -0,0 +1,81 @@ +package lol.pyr.znpcsplus.config; + +import lol.pyr.znpcsplus.storage.NpcStorageType; +import space.arim.dazzleconf.annote.ConfComments; +import space.arim.dazzleconf.annote.ConfKey; +import space.arim.dazzleconf.annote.SubSection; + +import static space.arim.dazzleconf.annote.ConfDefault.*; + +public interface MainConfig { + @ConfKey("view-distance") + @ConfComments("How far away do you need to be from any NPC for it to disappear, measured in blocks") + @DefaultInteger(32) + int viewDistance(); + + @ConfKey("line-spacing") + @ConfComments("The height between hologram lines, measured in blocks") + @DefaultDouble(0.3D) + double lineSpacing(); + + @ConfKey("check-for-updates") + @ConfComments("Should the plugin check for available updates and notify admins about them?") + @DefaultBoolean(true) + boolean checkForUpdates(); + + @ConfKey("debug-enabled") + @ConfComments({ + "Should debug mode be enabled?", + "This is used in development to test various things, you probably don't want to enable this" + }) + @DefaultBoolean(false) + boolean debugEnabled(); + + @ConfKey("storage-type") + @ConfComments("The storage type to use. Available storage types: YAML, SQLITE, MYSQL") + @DefaultString("YAML") + NpcStorageType storageType(); + + @ConfKey("database-config") + @ConfComments("The database config. Only used if storage-type is MYSQL") + @SubSection + DatabaseConfig databaseConfig(); + + @ConfKey("disable-skin-fetcher-warnings") + @ConfComments("Set this to true if you don't want to be warned in the console when a skin fails to resolve") + @DefaultBoolean(false) + boolean disableSkinFetcherWarnings(); + + @ConfKey("auto-save-interval") + @ConfComments("How often to auto-save npcs, set this to -1 to disable. This value will only apply on restart") + @DefaultInteger(300) + int autoSaveInterval(); + + default boolean autoSaveEnabled() { + return autoSaveInterval() != -1; + } + + @ConfKey("look-property-distance") + @ConfComments("How far should the look property work from in blocks") + @DefaultDouble(10) + double lookPropertyDistance(); + + @ConfKey("tab-hide-delay") + @ConfComments({ + "The amount of time to wait before removing the npc from the player list (aka tab) in ticks", + "If you're on 1.19.2 or above changing this value will have almost no effect since npcs are hidden in tab", + "WARNING: Setting this value too low may cause issues with player npcs spawning" + }) + @DefaultInteger(60) + int tabHideDelay(); + + @ConfKey("tab-display-name") + @ConfComments("The display name to use for npcs in the player list (aka tab)") + @DefaultString("ZNPC[{id}]") + String tabDisplayName(); + + @ConfKey("fake-enforce-secure-chat") + @ConfComments("Should the plugin fake the enforce secure chat packet to hide the popup?") + @DefaultBoolean(false) + boolean fakeEnforceSecureChat(); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/config/MessageConfig.java b/plugin/src/main/java/lol/pyr/znpcsplus/config/MessageConfig.java new file mode 100644 index 0000000..666a59e --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/config/MessageConfig.java @@ -0,0 +1,38 @@ +package lol.pyr.znpcsplus.config; + +/** + * (OLD CONFIGURATION) + * NO_PERMISSION("messages", "&cYou do not have permission to execute this command.", String.class), + * SUCCESS("messages", "&aSuccess!", String.class), + * INCORRECT_USAGE("messages", "&cThe arguments you specified are invalid. Type &f/znpcs&c for examples.", String.class), + * COMMAND_NOT_FOUND("messages", "&cThe command you specified does not exist!", String.class), + * COMMAND_ERROR("messages", "&cAn error occurred when executing this command. See console for more information.", String.class), + * INVALID_NUMBER("messages", "&cThe ID you have specified is invalid. Please use positive integers only!", String.class), + * NPC_NOT_FOUND("messages", "&cNo NPCs could be found with this ID!", String.class), + * TOO_FEW_ARGUMENTS("messages", "&cThis command does not contain enough arguments. Type &f/znpcs&c or view our documentation for a list/examples of existing arguments.", String.class), + * PATH_START("messages", "&aSuccess! Move to create a path for your NPC. When finished, type &f/znpcs path exit&c to exit path creation.", String.class), + * EXIT_PATH("messages", "&cYou have exited path creation.", String.class), + * PATH_FOUND("messages", "&cThere is already a path with this getName.", String.class), + * NPC_FOUND("messages", "&cThere is already an NPC with this ID.", String.class), + * NO_PATH_FOUND("messages", "&cThe path you have specified does not exist.", String.class), + * NO_SKIN_FOUND("messages", "&cThe skin username/URL you have specified does not exist or is invalid.", String.class), + * NO_NPC_FOUND("messages", "&cThe NPC you have specified does not exist.", String.class), + * NO_ACTION_FOUND("messages", "&cThis action does not exist! Type &f/znpcs&c or view our documentation for a list/examples of existing action types.", String.class), + * METHOD_NOT_FOUND("messages", "&cThis method does not exist! Type &f/znpcs&c or view our documentation for a list/examples of existing methods.", String.class), + * INVALID_NAME_LENGTH("messages", "&cThe getName you specified either too short or long. Please enter a positive integer of (3 to 16) characters.", String.class), + * UNSUPPORTED_ENTITY("messages", "&cThis entity type not available in your current server version.", String.class), + * PATH_SET_INCORRECT_USAGE("messages", "&eUsage: &aset ", String.class), + * ACTION_ADD_INCORRECT_USAGE("messages", "&eUsage: &a ", String.class), + * ACTION_DELAY_INCORRECT_USAGE("messages", "&eUsage: &a ", String.class), + * CONVERSATION_SET_INCORRECT_USAGE("messages", "&cUsage: ", String.class), + * NO_CONVERSATION_FOUND("messages", "&cThe conversation you have specified does not exist!", String.class), + * CONVERSATION_FOUND("messages", "&cThere is already a conversation with this getName.", String.class), + * INVALID_SIZE("messages", "&cThe position you have specified cannot exceed the limit.", String.class), + * FETCHING_SKIN("messages", "&aFetching skin for getName: &f%s&a. Please wait...", String.class), + * CANT_GET_SKIN("messages", "&cCould not fetch skin for getName: %s.", String.class), + * GET_SKIN("messages", "&aSkin successfully fetched!", String.class), + * NOT_SUPPORTED_NPC_TYPE("messages", "&cThis NPC type doesn't exists or is not supported in your current server version.", String.class); + */ + +public interface MessageConfig { +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/DataImporter.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/DataImporter.java new file mode 100644 index 0000000..3f932cc --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/DataImporter.java @@ -0,0 +1,10 @@ +package lol.pyr.znpcsplus.conversion; + +import lol.pyr.znpcsplus.npc.NpcEntryImpl; + +import java.util.Collection; + +public interface DataImporter { + Collection importData(); + boolean isValid(); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/DataImporterRegistry.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/DataImporterRegistry.java new file mode 100644 index 0000000..1185eb4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/DataImporterRegistry.java @@ -0,0 +1,54 @@ +package lol.pyr.znpcsplus.conversion; + +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.conversion.citizens.CitizensImporter; +import lol.pyr.znpcsplus.conversion.fancynpcs.FancyNpcsImporter; +import lol.pyr.znpcsplus.conversion.znpcs.ZNpcImporter; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.util.BungeeConnector; +import lol.pyr.znpcsplus.util.LazyLoader; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class DataImporterRegistry { + private final Map> importers = new HashMap<>(); + + public DataImporterRegistry(ConfigManager configManager, BukkitAudiences adventure, + TaskScheduler taskScheduler, PacketFactory packetFactory, LegacyComponentSerializer textSerializer, + NpcTypeRegistryImpl typeRegistry, File pluginsFolder, EntityPropertyRegistryImpl propertyRegistry, + MojangSkinCache skinCache, NpcRegistryImpl npcRegistry, BungeeConnector bungeeConnector) { + + register("znpcs", LazyLoader.of(() -> new ZNpcImporter(configManager, adventure, taskScheduler, + packetFactory, textSerializer, typeRegistry, propertyRegistry, skinCache, new File(pluginsFolder, "ServersNPC/data.json"), bungeeConnector))); + register("znpcsplus_legacy", LazyLoader.of(() -> new ZNpcImporter(configManager, adventure, taskScheduler, + packetFactory, textSerializer, typeRegistry, propertyRegistry, skinCache, new File(pluginsFolder, "ZNPCsPlusLegacy/data.json"), bungeeConnector))); + register("citizens", LazyLoader.of(() -> new CitizensImporter(configManager, adventure, taskScheduler, + packetFactory, textSerializer, typeRegistry, propertyRegistry, skinCache, new File(pluginsFolder, "Citizens/saves.yml"), npcRegistry))); + register("fancynpcs", LazyLoader.of(() -> new FancyNpcsImporter(configManager, adventure, taskScheduler, + packetFactory, textSerializer, typeRegistry, propertyRegistry, skinCache, new File(pluginsFolder, "FancyNpcs/npcs.yml"), npcRegistry))); + } + + private void register(String id, LazyLoader loader) { + importers.put(id.toLowerCase(), loader); + } + + public DataImporter getImporter(String id) { + id = id.toLowerCase(); + return importers.containsKey(id) ? importers.get(id).get() : null; + } + + public Collection getIds() { + return Collections.unmodifiableSet(importers.keySet()); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/CitizensImporter.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/CitizensImporter.java new file mode 100644 index 0000000..aecfb93 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/CitizensImporter.java @@ -0,0 +1,118 @@ +package lol.pyr.znpcsplus.conversion.citizens; + +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.conversion.DataImporter; +import lol.pyr.znpcsplus.conversion.citizens.model.CitizensTrait; +import lol.pyr.znpcsplus.conversion.citizens.model.CitizensTraitsRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.traits.TypeTrait; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.UUID; + +@SuppressWarnings("FieldCanBeLocal") +public class CitizensImporter implements DataImporter { + private final ConfigManager configManager; + private final BukkitAudiences adventure; + private final TaskScheduler scheduler; + private final PacketFactory packetFactory; + private final LegacyComponentSerializer textSerializer; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final MojangSkinCache skinCache; + private final File dataFile; + private final CitizensTraitsRegistry traitsRegistry; + private final NpcRegistryImpl npcRegistry; + + public CitizensImporter(ConfigManager configManager, BukkitAudiences adventure, + TaskScheduler taskScheduler, PacketFactory packetFactory, LegacyComponentSerializer textSerializer, + NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, MojangSkinCache skinCache, + File dataFile, NpcRegistryImpl npcRegistry) { + this.configManager = configManager; + this.adventure = adventure; + this.scheduler = taskScheduler; + this.packetFactory = packetFactory; + this.textSerializer = textSerializer; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.skinCache = skinCache; + this.dataFile = dataFile; + this.traitsRegistry = new CitizensTraitsRegistry(propertyRegistry, skinCache, taskScheduler, textSerializer); + this.npcRegistry = npcRegistry; + } + + @Override + public Collection importData() { + YamlConfiguration config = YamlConfiguration.loadConfiguration(dataFile); + ConfigurationSection npcsSection = config.getConfigurationSection("npc"); + if (npcsSection == null) { + return Collections.emptyList(); + } + ArrayList entries = new ArrayList<>(); + npcsSection.getKeys(false).forEach(key -> { + ConfigurationSection npcSection = npcsSection.getConfigurationSection(key); + if (npcSection == null) { + return; + } + String name = npcSection.getString("name", "Citizens NPC"); + UUID uuid; + try { + uuid = UUID.fromString(npcSection.getString("uuid")); + } catch (IllegalArgumentException e) { + uuid = UUID.randomUUID(); + } + String world = npcSection.getString("traits.location.world"); + if (world == null) { + world = Bukkit.getWorlds().get(0).getName(); + } + NpcImpl npc = new NpcImpl(uuid, propertyRegistry, configManager, packetFactory, textSerializer, world, typeRegistry.getByName("armor_stand"), new NpcLocation(0, 0, 0, 0, 0)); + + ConfigurationSection traits = npcSection.getConfigurationSection("traits"); + if (traits != null) { + TypeTrait typeTrait = new TypeTrait(typeRegistry); + npc = typeTrait.apply(npc, traits.getString("type")); + npc.getType().applyDefaultProperties(npc); + for (String traitName : traits.getKeys(false)) { + Object trait = traits.get(traitName); + CitizensTrait citizensTrait = traitsRegistry.getByName(traitName); + if (citizensTrait != null) { + npc = citizensTrait.apply(npc, trait); + } + } + } + boolean nameVisible = Boolean.parseBoolean(npcSection.getString("metadata.name-visible", "true")); + if (nameVisible) { + npc.getHologram().addTextLineComponent(textSerializer.deserialize(name)); + } + String id = key.toLowerCase(); + while (npcRegistry.getById(id) != null) { + id += "_"; // TODO: make a backup of the old npc instead + } + NpcEntryImpl entry = new NpcEntryImpl(id, npc); + entry.enableEverything(); + entries.add(entry); + }); + return entries; + } + + @Override + public boolean isValid() { + return dataFile.isFile(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/CitizensTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/CitizensTrait.java new file mode 100644 index 0000000..0811589 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/CitizensTrait.java @@ -0,0 +1,19 @@ +package lol.pyr.znpcsplus.conversion.citizens.model; + +import lol.pyr.znpcsplus.npc.NpcImpl; +import org.jetbrains.annotations.NotNull; + +public abstract class CitizensTrait { + private final String identifier; + + public CitizensTrait(String identifier) { + this.identifier = identifier; + } + + public String getIdentifier() { + return identifier; + } + + public abstract @NotNull NpcImpl apply(NpcImpl npc, Object value); + +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/CitizensTraitsRegistry.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/CitizensTraitsRegistry.java new file mode 100644 index 0000000..6b004e1 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/CitizensTraitsRegistry.java @@ -0,0 +1,35 @@ +package lol.pyr.znpcsplus.conversion.citizens.model; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.traits.*; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.util.HashMap; + +public class CitizensTraitsRegistry { + private final HashMap traitMap = new HashMap<>(); + + public CitizensTraitsRegistry(EntityPropertyRegistry propertyRegistry, MojangSkinCache skinCache, TaskScheduler taskScheduler, LegacyComponentSerializer textSerializer) { + register(new LocationTrait()); + register(new ProfessionTrait(propertyRegistry)); + register(new VillagerTrait(propertyRegistry)); + register(new SkinTrait(propertyRegistry)); + register(new MirrorTrait(propertyRegistry, skinCache)); + register(new SkinLayersTrait(propertyRegistry)); + register(new LookTrait(propertyRegistry)); + register(new CommandTrait(taskScheduler)); + register(new HologramTrait(textSerializer)); + register(new EquipmentTrait(propertyRegistry)); + register(new SpawnedTrait()); + } + + public CitizensTrait getByName(String name) { + return traitMap.get(name); + } + + public void register(CitizensTrait trait) { + traitMap.put(trait.getIdentifier(), trait); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/SectionCitizensTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/SectionCitizensTrait.java new file mode 100644 index 0000000..123eca5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/SectionCitizensTrait.java @@ -0,0 +1,19 @@ +package lol.pyr.znpcsplus.conversion.citizens.model; + +import lol.pyr.znpcsplus.npc.NpcImpl; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +public abstract class SectionCitizensTrait extends CitizensTrait { + public SectionCitizensTrait(String identifier) { + super(identifier); + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, Object value) { + if (!(value instanceof ConfigurationSection)) return npc; + return apply(npc, (ConfigurationSection) value); + } + + public abstract @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/StringCitizensTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/StringCitizensTrait.java new file mode 100644 index 0000000..58199a4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/StringCitizensTrait.java @@ -0,0 +1,18 @@ +package lol.pyr.znpcsplus.conversion.citizens.model; + +import lol.pyr.znpcsplus.npc.NpcImpl; +import org.jetbrains.annotations.NotNull; + +public abstract class StringCitizensTrait extends CitizensTrait { + public StringCitizensTrait(String identifier) { + super(identifier); + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, Object value) { + if (!(value instanceof String)) return npc; + return apply(npc, (String) value); + } + + public abstract @NotNull NpcImpl apply(NpcImpl npc, String string); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/CommandTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/CommandTrait.java new file mode 100644 index 0000000..e6629f8 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/CommandTrait.java @@ -0,0 +1,69 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.interaction.consolecommand.ConsoleCommandAction; +import lol.pyr.znpcsplus.interaction.playercommand.PlayerCommandAction; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +public class CommandTrait extends SectionCitizensTrait { + private final TaskScheduler scheduler; + + public CommandTrait(TaskScheduler scheduler) { + super("commandtrait"); + this.scheduler = scheduler; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + ConfigurationSection commands = section.getConfigurationSection("commands"); + if (commands != null) { + Set keys = commands.getKeys(false); + if (keys != null) { + for (String key : keys) { + ConfigurationSection commandSection = commands.getConfigurationSection(key); + String command = commandSection.getString("command"); + String hand = commandSection.getString("hand", "BOTH"); + InteractionType clickType = wrapClickType(hand); + boolean isPlayerCommand = commandSection.getBoolean("player", true); + int cooldown = commandSection.getInt("cooldown", 0); + int delay = commandSection.getInt("delay", 0); + if (command != null) { + InteractionAction action; + if (isPlayerCommand) { + action = new PlayerCommandAction(scheduler, command, clickType, cooldown, delay); + } else { + action = new ConsoleCommandAction(scheduler, command, clickType, cooldown, delay); + } + npc.addAction(action); + } + } + } + } + return npc; + } + + private InteractionType wrapClickType(String hand) { + if (hand == null) { + return InteractionType.ANY_CLICK; + } + switch (hand) { + case "RIGHT": + case "SHIFT_RIGHT": + return InteractionType.RIGHT_CLICK; + case "LEFT": + case "SHIFT_LEFT": + return InteractionType.LEFT_CLICK; + case "BOTH": + return InteractionType.ANY_CLICK; + } + throw new IllegalStateException(); + } + +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/EquipmentTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/EquipmentTrait.java new file mode 100644 index 0000000..1f3393a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/EquipmentTrait.java @@ -0,0 +1,115 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import com.github.retrooper.packetevents.protocol.player.EquipmentSlot; +import com.google.common.io.BaseEncoding; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class EquipmentTrait extends SectionCitizensTrait { + private final EntityPropertyRegistry propertyRegistry; + private final HashMap EQUIPMENT_SLOT_MAP = new HashMap<>(); + + public EquipmentTrait(EntityPropertyRegistry propertyRegistry) { + super("equipment"); + this.propertyRegistry = propertyRegistry; + EQUIPMENT_SLOT_MAP.put("hand", EquipmentSlot.MAIN_HAND); + EQUIPMENT_SLOT_MAP.put("offhand", EquipmentSlot.OFF_HAND); + EQUIPMENT_SLOT_MAP.put("helmet", EquipmentSlot.HELMET); + EQUIPMENT_SLOT_MAP.put("chestplate", EquipmentSlot.CHEST_PLATE); + EQUIPMENT_SLOT_MAP.put("leggings", EquipmentSlot.LEGGINGS); + EQUIPMENT_SLOT_MAP.put("boots", EquipmentSlot.BOOTS); + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + for (String key : section.getKeys(false)) { + EquipmentSlot slot = EQUIPMENT_SLOT_MAP.get(key); + if (slot == null) { + continue; + } + + ItemStack itemStack = parseItemStack(section.getConfigurationSection(key)); + if (itemStack == null) { + continue; + } + + EntityProperty property = propertyRegistry.getByName(key, ItemStack.class); + npc.setProperty(property, itemStack); + } + + return npc; + } + + private ItemStack parseItemStack(ConfigurationSection section) { + Material material = null; + if (section.isString("type_key")) { + material = Material.getMaterial(section.getString("type_key").toUpperCase()); + } else if (section.isString("type")) { + material = Material.matchMaterial(section.getString("type").toUpperCase()); + } else if (section.isString("id")) { + material = Material.matchMaterial(section.getString("id").toUpperCase()); + } + if (material == null || material == Material.AIR) { + return null; + } + org.bukkit.inventory.ItemStack itemStack = new org.bukkit.inventory.ItemStack(material, section.getInt("amount", 1), + (short) section.getInt("durability", section.getInt("data", 0))); + if (section.isInt("mdata")) { + //noinspection deprecation + itemStack.getData().setData((byte) section.getInt("mdata")); + } + if (section.isConfigurationSection("enchantments")) { + ConfigurationSection enchantments = section.getConfigurationSection("enchantments"); + itemStack.addUnsafeEnchantments(deserializeEnchantments(enchantments)); + } + if (section.isConfigurationSection("meta")) { + ItemMeta itemMeta = deserializeMeta(section.getConfigurationSection("meta")); + if (itemMeta != null) { + itemStack.setItemMeta(itemMeta); + } + } + return SpigotConversionUtil.fromBukkitItemStack(itemStack); + } + + private Map deserializeEnchantments(ConfigurationSection section) { + Map enchantments = new HashMap<>(); + for (String key : section.getKeys(false)) { + Enchantment enchantment = Enchantment.getByName(key); + if (enchantment == null) { + continue; + } + enchantments.put(enchantment, section.getInt(key)); + } + return enchantments; + } + + private ItemMeta deserializeMeta(ConfigurationSection section) { + if (section.isString("encoded-meta")) { + byte[] raw = BaseEncoding.base64().decode(section.getString("encoded-meta")); + try { + BukkitObjectInputStream inp = new BukkitObjectInputStream(new ByteArrayInputStream(raw)); + ItemMeta meta = (ItemMeta) inp.readObject(); + inp.close(); + return meta; + } catch (IOException | ClassNotFoundException e1) { + e1.printStackTrace(); + } + } + return null; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/HologramTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/HologramTrait.java new file mode 100644 index 0000000..48c850d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/HologramTrait.java @@ -0,0 +1,36 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class HologramTrait extends SectionCitizensTrait { + private final LegacyComponentSerializer textSerializer; + + public HologramTrait(LegacyComponentSerializer textSerializer) { + super("hologramtrait"); + this.textSerializer = textSerializer; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + ConfigurationSection linesSection = section.getConfigurationSection("lines"); + if (linesSection != null) { + List keys = new ArrayList<>(linesSection.getKeys(false)); + for (int i = keys.size() - 1; i >= 0; i--) { + String line = linesSection.isConfigurationSection(keys.get(i)) ? linesSection.getConfigurationSection(keys.get(i)).getString("text") : linesSection.getString(keys.get(i)); + if (line != null) { + Component component = textSerializer.deserialize(line); + npc.getHologram().addTextLineComponent(component); + } + } + } + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/LocationTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/LocationTrait.java new file mode 100644 index 0000000..c56793c --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/LocationTrait.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +public class LocationTrait extends SectionCitizensTrait { + public LocationTrait() { + super("location"); + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + double x = Double.parseDouble(section.getString("x")); + double y = Double.parseDouble(section.getString("y")); + double z = Double.parseDouble(section.getString("z")); + float yaw = Float.parseFloat(section.getString("yaw")); + float pitch = Float.parseFloat(section.getString("pitch")); + NpcLocation location = new NpcLocation(x, y, z, yaw, pitch); + npc.setLocation(location); + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/LookTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/LookTrait.java new file mode 100644 index 0000000..650b6a6 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/LookTrait.java @@ -0,0 +1,23 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.util.LookType; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +public class LookTrait extends SectionCitizensTrait { + private final EntityPropertyRegistry registry; + + public LookTrait(EntityPropertyRegistry registry) { + super("lookclose"); + this.registry = registry; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + if (section.getBoolean("enabled")) npc.setProperty(registry.getByName("look", LookType.class), LookType.CLOSEST_PLAYER); + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/MirrorTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/MirrorTrait.java new file mode 100644 index 0000000..19360bf --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/MirrorTrait.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.skin.descriptor.MirrorDescriptor; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +public class MirrorTrait extends SectionCitizensTrait { + private final EntityPropertyRegistry registry; + private final MojangSkinCache skinCache; + + public MirrorTrait(EntityPropertyRegistry registry, MojangSkinCache skinCache) { + super("mirrortrait"); + this.registry = registry; + this.skinCache = skinCache; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + if (section.getBoolean("enabled")) npc.setProperty(registry.getByName("skin", SkinDescriptor.class), new MirrorDescriptor(skinCache)); + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/ProfessionTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/ProfessionTrait.java new file mode 100644 index 0000000..551d9a8 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/ProfessionTrait.java @@ -0,0 +1,28 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.StringCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.util.VillagerProfession; +import org.jetbrains.annotations.NotNull; + +public class ProfessionTrait extends StringCitizensTrait { + private final EntityPropertyRegistry registry; + + public ProfessionTrait(EntityPropertyRegistry registry) { + super("profession"); + this.registry = registry; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, String string) { + VillagerProfession profession; + try { + profession = VillagerProfession.valueOf(string.toUpperCase()); + } catch (IllegalArgumentException ignored) { + profession = VillagerProfession.NONE; + } + npc.setProperty(registry.getByName("villager_profession", VillagerProfession.class), profession); + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SkinLayersTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SkinLayersTrait.java new file mode 100644 index 0000000..f2d7ed0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SkinLayersTrait.java @@ -0,0 +1,38 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +public class SkinLayersTrait extends SectionCitizensTrait { + private final EntityPropertyRegistry registry; + private final Map skinLayers; + + public SkinLayersTrait(EntityPropertyRegistry registry) { + super("skinlayers"); + this.registry = registry; + this.skinLayers = new HashMap<>(); + this.skinLayers.put("cape", "skin_cape"); + this.skinLayers.put("hat", "skin_hat"); + this.skinLayers.put("jacket", "skin_jacket"); + this.skinLayers.put("left_sleeve", "skin_left_sleeve"); + this.skinLayers.put("left_pants", "skin_left_leg"); + this.skinLayers.put("right_sleeve", "skin_right_sleeve"); + this.skinLayers.put("right_pants", "skin_right_leg"); + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + for (Map.Entry entry : this.skinLayers.entrySet()) { + String key = entry.getKey(); + String property = entry.getValue(); + if (section.contains(key)) npc.setProperty(registry.getByName(property, Boolean.class), section.getBoolean(key)); + } + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SkinTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SkinTrait.java new file mode 100644 index 0000000..d21ab4e --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SkinTrait.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +public class SkinTrait extends SectionCitizensTrait { + private final EntityPropertyRegistry registry; + + public SkinTrait(EntityPropertyRegistry registry) { + super("skintrait"); + this.registry = registry; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + String texture = section.getString("textureRaw"); + String signature = section.getString("signature"); + if (texture != null && signature != null) npc.setProperty(registry.getByName("skin", SkinDescriptor.class), new PrefetchedDescriptor(new SkinImpl(texture, signature))); + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SpawnedTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SpawnedTrait.java new file mode 100644 index 0000000..9f080c5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SpawnedTrait.java @@ -0,0 +1,20 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.conversion.citizens.model.CitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import org.jetbrains.annotations.NotNull; + +public class SpawnedTrait extends CitizensTrait { + + public SpawnedTrait() { + super("spawned"); + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, Object value) { + if (value != null) { + npc.setEnabled((boolean) value); + } + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/TypeTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/TypeTrait.java new file mode 100644 index 0000000..6af1d85 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/TypeTrait.java @@ -0,0 +1,31 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.npc.NpcTypeRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.StringCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcTypeImpl; +import org.jetbrains.annotations.NotNull; + +public class TypeTrait extends StringCitizensTrait { + private final NpcTypeRegistry registry; + + public TypeTrait(NpcTypeRegistry registry) { + super("type"); + this.registry = registry; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, String string) { + NpcTypeImpl type = warpNpcType(string); + if (type == null) return npc; + npc.setType(type); + return npc; + } + + private NpcTypeImpl warpNpcType(String name) { + name = name.toLowerCase(); +// if (name.equals("player")) name = "human"; +// else if (name.equals("zombievillager")) name = "zombie_villager"; + return (NpcTypeImpl) registry.getByName(name); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/VillagerTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/VillagerTrait.java new file mode 100644 index 0000000..4998f78 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/VillagerTrait.java @@ -0,0 +1,39 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.util.VillagerLevel; +import lol.pyr.znpcsplus.util.VillagerType; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +public class VillagerTrait extends SectionCitizensTrait { + private final EntityPropertyRegistry registry; + + public VillagerTrait(EntityPropertyRegistry registry) { + super("villagertrait"); + this.registry = registry; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + int level = section.getInt("level"); + String type = section.getString("type", "plains"); + VillagerLevel villagerLevel; + try { + villagerLevel = VillagerLevel.values()[level]; + } catch (ArrayIndexOutOfBoundsException ignored) { + villagerLevel = VillagerLevel.STONE; + } + VillagerType villagerType; + try { + villagerType = VillagerType.valueOf(type.toUpperCase()); + } catch (IllegalArgumentException ignored) { + villagerType = VillagerType.PLAINS; + } + npc.setProperty(registry.getByName("villager_level", VillagerLevel.class), villagerLevel); + npc.setProperty(registry.getByName("villager_type", VillagerType.class), villagerType); + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/fancynpcs/FancyNpcsImporter.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/fancynpcs/FancyNpcsImporter.java new file mode 100644 index 0000000..b5c981a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/fancynpcs/FancyNpcsImporter.java @@ -0,0 +1,183 @@ +package lol.pyr.znpcsplus.conversion.fancynpcs; + +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.conversion.DataImporter; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.interaction.consolecommand.ConsoleCommandAction; +import lol.pyr.znpcsplus.interaction.message.MessageAction; +import lol.pyr.znpcsplus.interaction.playercommand.PlayerCommandAction; +import lol.pyr.znpcsplus.npc.*; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.skin.descriptor.MirrorDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor; +import lol.pyr.znpcsplus.util.LookType; +import lol.pyr.znpcsplus.util.NamedColor; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.inventory.ItemStack; + +import java.io.File; +import java.util.*; + +public class FancyNpcsImporter implements DataImporter { + private final ConfigManager configManager; + private final BukkitAudiences adventure; + private final TaskScheduler scheduler; + private final PacketFactory packetFactory; + private final LegacyComponentSerializer textSerializer; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final MojangSkinCache skinCache; + private final File dataFile; + private final NpcRegistryImpl npcRegistry; + + public FancyNpcsImporter(ConfigManager configManager, BukkitAudiences adventure, + TaskScheduler taskScheduler, PacketFactory packetFactory, LegacyComponentSerializer textSerializer, + NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, MojangSkinCache skinCache, + File dataFile, NpcRegistryImpl npcRegistry) { + this.configManager = configManager; + this.adventure = adventure; + this.scheduler = taskScheduler; + this.packetFactory = packetFactory; + this.textSerializer = textSerializer; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.skinCache = skinCache; + this.dataFile = dataFile; + this.npcRegistry = npcRegistry; + } + + @Override + public Collection importData() { + YamlConfiguration config = YamlConfiguration.loadConfiguration(dataFile); + ConfigurationSection npcsSection = config.getConfigurationSection("npcs"); + if (npcsSection == null) { + return Collections.emptyList(); + } + ArrayList entries = new ArrayList<>(); + npcsSection.getKeys(false).forEach(key -> { + ConfigurationSection npcSection = npcsSection.getConfigurationSection(key); + if (npcSection == null) { + return; + } + String name = npcSection.getString("name", "FancyNPC"); + UUID uuid = UUID.fromString(key); + String world = npcSection.getString("location.world"); + if (world == null) { + world = Bukkit.getWorlds().get(0).getName(); + } + NpcLocation location = new NpcLocation( + npcSection.getDouble("location.x"), + npcSection.getDouble("location.y"), + npcSection.getDouble("location.z"), + (float) npcSection.getDouble("location.yaw"), + (float) npcSection.getDouble("location.pitch") + ); + String typeString = npcSection.getString("type"); + NpcTypeImpl type = typeRegistry.getByName(typeString); + if (type == null) { + type = typeRegistry.getByName("player"); + } + NpcImpl npc = new NpcImpl(uuid, propertyRegistry, configManager, packetFactory, textSerializer, world, type, location); + npc.getType().applyDefaultProperties(npc); + + npc.getHologram().addTextLineComponent(textSerializer.deserialize(name)); + boolean glowing = npcSection.getBoolean("glowing", false); + if (glowing) { + NamedColor color; + try { + color = NamedColor.valueOf(npcSection.getString("glowingColor", "white")); + } catch (IllegalArgumentException ignored) { + color = NamedColor.WHITE; + } + EntityPropertyImpl property = propertyRegistry.getByName("glow", NamedColor.class); + npc.setProperty(property, color); + } + if (npcSection.getBoolean("turnToPlayer", false)) { + EntityPropertyImpl property = propertyRegistry.getByName("look", LookType.class); + npc.setProperty(property, LookType.CLOSEST_PLAYER); + } + if (npcSection.isConfigurationSection("skin")) { + ConfigurationSection skinSection = npcSection.getConfigurationSection("skin"); + String texture = skinSection.getString("value"); + String signature = skinSection.getString("signature"); + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new PrefetchedDescriptor(new SkinImpl(texture, signature))); + } + if (npcSection.isConfigurationSection("equipment")) { + ConfigurationSection equipmentSection = npcSection.getConfigurationSection("equipment"); + for (String slot : equipmentSection.getKeys(false)) { + ItemStack item = equipmentSection.getItemStack(slot); + if (item != null) { + npc.setProperty(propertyRegistry.getByName(getEquipmentPropertyName(slot), + com.github.retrooper.packetevents.protocol.item.ItemStack.class), SpigotConversionUtil.fromBukkitItemStack(item)); + } + } + } + if (npcSection.getBoolean("mirrorSkin")) { + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new MirrorDescriptor(skinCache)); + } + List playerCommands = npcSection.getStringList("playerCommands"); + if (!playerCommands.isEmpty()) { + long cooldown = npcSection.getLong("interactionCooldown", 0); + for (String command : playerCommands) { + npc.addAction(new PlayerCommandAction(scheduler, command, InteractionType.ANY_CLICK, cooldown, 0)); + } + } + String serverCommand = npcSection.getString("serverCommand"); + if (serverCommand != null) { + long cooldown = npcSection.getLong("interactionCooldown", 0); + npc.addAction(new ConsoleCommandAction(scheduler, serverCommand, InteractionType.ANY_CLICK, cooldown, 0)); + } + List messages = npcSection.getStringList("messages"); + if (!messages.isEmpty()) { + long cooldown = npcSection.getLong("interactionCooldown", 0); + for (String message : messages) { + npc.addAction(new MessageAction(adventure, textSerializer, message, InteractionType.ANY_CLICK, cooldown, 0)); + } + } + String id = npcSection.getString("name"); + while (npcRegistry.getById(id) != null) { + id += "_"; + } + NpcEntryImpl entry = new NpcEntryImpl(id, npc); + entry.enableEverything(); + entries.add(entry); + }); + return entries; + } + + private String getEquipmentPropertyName(String slot) { + switch (slot) { + case "MAINHAND": + return "hand"; + case "OFFHAND": + return "offhand"; + case "FEET": + return "boots"; + case "LEGS": + return "leggings"; + case "CHEST": + return "chestplate"; + case "HEAD": + return "helmet"; + default: + return null; + } + } + + @Override + public boolean isValid() { + return dataFile.isFile(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/ZNpcImporter.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/ZNpcImporter.java new file mode 100644 index 0000000..024c66c --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/ZNpcImporter.java @@ -0,0 +1,243 @@ +package lol.pyr.znpcsplus.conversion.znpcs; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.conversion.DataImporter; +import lol.pyr.znpcsplus.conversion.znpcs.model.*; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.interaction.consolecommand.ConsoleCommandAction; +import lol.pyr.znpcsplus.interaction.message.MessageAction; +import lol.pyr.znpcsplus.interaction.playerchat.PlayerChatAction; +import lol.pyr.znpcsplus.interaction.playercommand.PlayerCommandAction; +import lol.pyr.znpcsplus.interaction.switchserver.SwitchServerAction; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.skin.descriptor.NameFetchingDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.MirrorDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor; +import lol.pyr.znpcsplus.util.BungeeConnector; +import lol.pyr.znpcsplus.util.ItemSerializationUtil; +import lol.pyr.znpcsplus.util.LookType; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.DyeColor; +import org.bukkit.inventory.ItemStack; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.*; + +public class ZNpcImporter implements DataImporter { + private final ConfigManager configManager; + private final BukkitAudiences adventure; + private final TaskScheduler taskScheduler; + private final PacketFactory packetFactory; + private final LegacyComponentSerializer textSerializer; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final MojangSkinCache skinCache; + private final File dataFile; + private final File conversationFile; + private final Gson gson; + private final BungeeConnector bungeeConnector; + + public ZNpcImporter(ConfigManager configManager, BukkitAudiences adventure, + TaskScheduler taskScheduler, PacketFactory packetFactory, LegacyComponentSerializer textSerializer, + NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, MojangSkinCache skinCache, File dataFile, BungeeConnector bungeeConnector) { + + this.configManager = configManager; + this.adventure = adventure; + this.taskScheduler = taskScheduler; + this.packetFactory = packetFactory; + this.textSerializer = textSerializer; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.skinCache = skinCache; + this.dataFile = dataFile; + this.conversationFile = new File(dataFile.getParentFile(), "conversations.json"); + this.bungeeConnector = bungeeConnector; + gson = new GsonBuilder() + .create(); + } + + @Override + public Collection importData() { + ZNpcsModel[] models; + try (BufferedReader fileReader = Files.newBufferedReader(dataFile.toPath())) { + JsonElement element = JsonParser.parseReader(fileReader); + models = gson.fromJson(element, ZNpcsModel[].class); + } catch (IOException e) { + e.printStackTrace(); + return Collections.emptyList(); + } + if (models == null) return Collections.emptyList(); + + + ZnpcsConversations[] conversations; + try (BufferedReader fileReader = Files.newBufferedReader(conversationFile.toPath())) { + JsonElement element = JsonParser.parseReader(fileReader); + conversations = gson.fromJson(element, ZnpcsConversations[].class); + } catch (IOException e) { + e.printStackTrace(); + return Collections.emptyList(); + } + if (conversations == null) return Collections.emptyList(); + + + ArrayList entries = new ArrayList<>(models.length); + for (ZNpcsModel model : models) { + String type = model.getNpcType(); + + switch (type.toLowerCase()) { + case "mushroom_cow": + type = "mooshroom"; + break; + case "snowman": + type = "snow_golem"; + break; + } + + ZNpcsLocation oldLoc = model.getLocation(); + NpcLocation location = new NpcLocation(oldLoc.getX(), oldLoc.getY(), oldLoc.getZ(), oldLoc.getYaw(), oldLoc.getPitch()); + UUID uuid = model.getUuid() == null ? UUID.randomUUID() : model.getUuid(); + NpcImpl npc = new NpcImpl(uuid, propertyRegistry, configManager, packetFactory, textSerializer, oldLoc.getWorld(), typeRegistry.getByName(type), location); + npc.getType().applyDefaultProperties(npc); + + + // Convert the conversations from each NPC + ZNpcsConversation conversation = model.getConversation(); + if (conversation != null) { + + // Loop through all conversations in the conversations.json file + for (ZnpcsConversations conv : conversations) { + + // If the conversation name matches the conversation name in the data.json file, proceed + if (conv.getName().equalsIgnoreCase(conversation.getConversationName())) { + + int totalDelay = 0; + + // Loop through all texts in the conversation + for(ZNpcsConversationText text : conv.getTexts()) { + + // Add the delay in ticks to the total delay + totalDelay += text.getDelay() * 20; + + // Get the lines of text from the conversation + String[] lines = text.getLines(); + + // Loop through all lines of text + for (String line : lines) { + + // Create a new message action for each line of text + InteractionAction action = new MessageAction(adventure, textSerializer, line, InteractionType.ANY_CLICK, 0, totalDelay); + npc.addAction(action); + } + } + } + } + } + + + HologramImpl hologram = npc.getHologram(); + hologram.setOffset(model.getHologramHeight()); + Collections.reverse(model.getHologramLines()); + for (String raw : model.getHologramLines()) { + Component line = textSerializer.deserialize(raw); + hologram.addTextLineComponent(line); + } + + for (ZNpcsAction action : model.getClickActions()) { + InteractionType t = adaptClickType(action.getClickType()); + npc.addAction(adaptAction(action.getActionType(), t, action.getAction(), action.getDelay())); + } + + for (Map.Entry entry : model.getNpcEquip().entrySet()) { + EntityPropertyImpl property = propertyRegistry.getByName(entry.getKey(), ItemStack.class); + if (property == null) continue; + npc.setProperty(property, ItemSerializationUtil.itemFromB64(entry.getValue())); + } + + if (model.getSkinName() != null) { + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new NameFetchingDescriptor(skinCache, model.getSkinName())); + } + else if (model.getSkin() != null && model.getSignature() != null) { + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new PrefetchedDescriptor(new SkinImpl(model.getSkin(), model.getSignature()))); + } + + Map toggleValues = model.getNpcToggleValues() == null ? model.getNpcFunctions() : model.getNpcToggleValues(); + if (toggleValues != null) { + if (toggleValues.containsKey("look")) { + npc.setProperty(propertyRegistry.getByName("look", LookType.class), LookType.CLOSEST_PLAYER); + } + if (toggleValues.containsKey("mirror")) { + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new MirrorDescriptor(skinCache)); + } + if (toggleValues.containsKey("glow") && (boolean) toggleValues.get("glow")) { + if (!model.getGlowName().isEmpty()) + try { + npc.setProperty(propertyRegistry.getByName("glow", DyeColor.class), DyeColor.valueOf(model.getGlowName())); + } catch (IllegalArgumentException e) { + npc.setProperty(propertyRegistry.getByName("glow", DyeColor.class), DyeColor.WHITE); + } + else + npc.setProperty(propertyRegistry.getByName("glow", DyeColor.class), DyeColor.WHITE); + } + } + + NpcEntryImpl entry = new NpcEntryImpl(String.valueOf(model.getId()), npc); + entry.enableEverything(); + entries.add(entry); + } + return entries; + } + + @Override + public boolean isValid() { + return dataFile.isFile(); + } + + private InteractionType adaptClickType(String clickType) { + switch (clickType.toLowerCase()) { + case "default": + return InteractionType.ANY_CLICK; + case "left": + return InteractionType.LEFT_CLICK; + case "right": + return InteractionType.RIGHT_CLICK; + } + throw new IllegalArgumentException("Couldn't adapt znpcs click type: " + clickType); + } + + private InteractionAction adaptAction(String type, InteractionType clickType, String parameter, int cooldown) { + switch (type.toLowerCase()) { + case "cmd": + return new PlayerCommandAction(taskScheduler, parameter, clickType, cooldown * 1000L, 0); + case "console": + return new ConsoleCommandAction(taskScheduler, parameter, clickType, cooldown * 1000L, 0); + case "chat": + return new PlayerChatAction(taskScheduler, parameter, clickType, cooldown * 1000L, 0); + case "message": + return new MessageAction(adventure, textSerializer, parameter, clickType, cooldown * 1000L, 0); + case "server": + return new SwitchServerAction(bungeeConnector, parameter, clickType, cooldown * 1000L, 0); + } + throw new IllegalArgumentException("Couldn't adapt znpcs click action: " + type); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsAction.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsAction.java new file mode 100644 index 0000000..4092fa4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsAction.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.conversion.znpcs.model; + +@SuppressWarnings("unused") +public class ZNpcsAction { + private String actionType; + private String clickType; + private String action; + private int delay; + + public String getActionType() { + return actionType; + } + + public String getClickType() { + return clickType; + } + + public String getAction() { + return action; + } + + public int getDelay() { + return delay; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsConversation.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsConversation.java new file mode 100644 index 0000000..eb80d28 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsConversation.java @@ -0,0 +1,16 @@ +package lol.pyr.znpcsplus.conversion.znpcs.model; + +@SuppressWarnings("unused") +public class ZNpcsConversation { + + private String conversationName; + private String conversationType; + + public String getConversationName() { + return conversationName; + } + + public String getConversationType() { + return conversationType; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsConversationText.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsConversationText.java new file mode 100644 index 0000000..e9a85b5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsConversationText.java @@ -0,0 +1,19 @@ +package lol.pyr.znpcsplus.conversion.znpcs.model; + +@SuppressWarnings("unused") +public class ZNpcsConversationText { + + private String[] lines; + private ZNpcsAction[] actions; + private int delay; + + public String[] getLines() { + return lines; + } + public ZNpcsAction[] getActions() { + return actions; + } + public int getDelay() { + return delay; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsLocation.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsLocation.java new file mode 100644 index 0000000..b585cce --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsLocation.java @@ -0,0 +1,35 @@ +package lol.pyr.znpcsplus.conversion.znpcs.model; + +@SuppressWarnings("unused") +public class ZNpcsLocation { + private String world; + private double x; + private double y; + private double z; + private float yaw; + private float pitch; + + public String getWorld() { + return world; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double getZ() { + return z; + } + + public float getYaw() { + return yaw; + } + + public float getPitch() { + return pitch; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsModel.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsModel.java new file mode 100644 index 0000000..987a9b4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsModel.java @@ -0,0 +1,91 @@ +package lol.pyr.znpcsplus.conversion.znpcs.model; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@SuppressWarnings("unused") +public class ZNpcsModel { + private int id; + private UUID uuid; + private double hologramHeight; + private String skinName; + private String skin; + private String signature; + + private String glowName; + + private ZNpcsConversation conversation; + private ZNpcsLocation location; + private String npcType; + private List hologramLines; + private List clickActions; + private Map npcEquip; + private Map npcToggleValues; + private Map npcFunctions; + private Map customizationMap; + + public int getId() { + return id; + } + + public UUID getUuid() { + return uuid; + } + + public double getHologramHeight() { + return hologramHeight; + } + + public String getSkinName() { + return skinName; + } + + public ZNpcsConversation getConversation() { + return conversation; + } + + public ZNpcsLocation getLocation() { + return location; + } + + public String getNpcType() { + return npcType; + } + + public List getHologramLines() { + return hologramLines; + } + + public List getClickActions() { + return clickActions; + } + + public Map getNpcEquip() { + return npcEquip; + } + + public Map getNpcToggleValues() { + return npcToggleValues; + } + + public Map getNpcFunctions() { + return npcFunctions; + } + + public Map getCustomizationMap() { + return customizationMap; + } + + public String getSkin() { + return skin; + } + + public String getSignature() { + return signature; + } + + public String getGlowName() { + return glowName; + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZnpcsConversations.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZnpcsConversations.java new file mode 100644 index 0000000..49cd913 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZnpcsConversations.java @@ -0,0 +1,23 @@ +package lol.pyr.znpcsplus.conversion.znpcs.model; + +@SuppressWarnings("unused") +public class ZnpcsConversations { + + private String name; + private ZNpcsConversationText[] texts; + private int radius; + private int delay; + + public String getName() { + return name; + } + public ZNpcsConversationText[] getTexts() { + return texts; + } + public int getRadius() { + return radius; + } + public int getDelay() { + return delay; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/ArmorStandVehicleProperties.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/ArmorStandVehicleProperties.java new file mode 100644 index 0000000..4e4605a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/ArmorStandVehicleProperties.java @@ -0,0 +1,76 @@ +package lol.pyr.znpcsplus.entity; + +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import org.bukkit.inventory.ItemStack; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Represents an armor stand vehicle entity. + *

+ * This entity is used to make the NPC sit on an invisible armor stand. + *

+ */ +public class ArmorStandVehicleProperties implements PropertyHolder { + + private final Map, Object> propertyMap = new HashMap<>(); + + public ArmorStandVehicleProperties(EntityPropertyRegistryImpl propertyRegistry) { + _setProperty(propertyRegistry.getByName("small", Boolean.class), true); + _setProperty(propertyRegistry.getByName("invisible", Boolean.class), true); + _setProperty(propertyRegistry.getByName("base_plate", Boolean.class), false); + } + + @SuppressWarnings("unchecked") + public T getProperty(EntityProperty key) { + return hasProperty(key) ? (T) propertyMap.get((EntityPropertyImpl) key) : key.getDefaultValue(); + } + + @Override + public boolean hasProperty(EntityProperty key) { + return propertyMap.containsKey((EntityPropertyImpl) key); + } + + @SuppressWarnings("unchecked") + private void _setProperty(EntityProperty key, T value) { + Object val = value; + if (val instanceof ItemStack) val = SpigotConversionUtil.fromBukkitItemStack((ItemStack) val); + + setProperty((EntityPropertyImpl) key, (T) val); + } + + @Override + public void setProperty(EntityProperty key, T value) { + throw new UnsupportedOperationException("Cannot set properties on armor stands"); + } + + @Override + public void setItemProperty(EntityProperty key, ItemStack value) { + throw new UnsupportedOperationException("Cannot set item properties on armor stands"); + } + + @Override + public ItemStack getItemProperty(EntityProperty key) { + throw new UnsupportedOperationException("Cannot get item properties on armor stands"); + } + + public void setProperty(EntityPropertyImpl key, T value) { + if (key == null) return; + if (value == null || value.equals(key.getDefaultValue())) propertyMap.remove(key); + else propertyMap.put(key, value); + } + + public Set> getAllProperties() { + return Collections.unmodifiableSet(propertyMap.keySet()); + } + + @Override + public Set> getAppliedProperties() { + return Collections.unmodifiableSet(propertyMap.keySet()); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyImpl.java new file mode 100644 index 0000000..dcfdf5f --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyImpl.java @@ -0,0 +1,62 @@ +package lol.pyr.znpcsplus.entity; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataType; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import org.bukkit.entity.Player; + +import java.util.*; + +public abstract class EntityPropertyImpl implements EntityProperty { + private final String name; + private final T defaultValue; + private final Class clazz; + private final Set> dependencies = new HashSet<>(); + private boolean playerModifiable = true; + + protected EntityPropertyImpl(String name, T defaultValue, Class clazz) { + this.name = name.toLowerCase(); + this.defaultValue = defaultValue; + this.clazz = clazz; + } + + @Override + public String getName() { + return name; + } + + @Override + public T getDefaultValue() { + return defaultValue; + } + + @Override + public boolean isPlayerModifiable() { + return playerModifiable; + } + + public void setPlayerModifiable(boolean playerModifiable) { + this.playerModifiable = playerModifiable; + } + + public Class getType() { + return clazz; + } + + public void addDependency(EntityPropertyImpl property) { + dependencies.add(property); + } + + protected static EntityData newEntityData(int index, EntityDataType type, V value) { + return new EntityData<>(index, type, value); + } + + public List> applyStandalone(Player player, PacketEntity packetEntity, boolean isSpawned) { + Map> map = new HashMap<>(); + apply(player, packetEntity, isSpawned, map); + for (EntityPropertyImpl property : dependencies) property.apply(player, packetEntity, isSpawned, map); + return new ArrayList<>(map.values()); + } + + abstract public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties); +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyRegistryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyRegistryImpl.java new file mode 100644 index 0000000..28053e6 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyRegistryImpl.java @@ -0,0 +1,800 @@ +package lol.pyr.znpcsplus.entity; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.protocol.attribute.Attributes; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.entity.pose.EntityPose; +import com.github.retrooper.packetevents.protocol.nbt.NBTCompound; +import com.github.retrooper.packetevents.protocol.nbt.NBTInt; +import com.github.retrooper.packetevents.protocol.nbt.NBTString; +import com.github.retrooper.packetevents.protocol.player.EquipmentSlot; +import com.github.retrooper.packetevents.protocol.world.BlockFace; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.properties.*; +import lol.pyr.znpcsplus.entity.properties.attributes.AttributeProperty; +import lol.pyr.znpcsplus.entity.properties.villager.VillagerLevelProperty; +import lol.pyr.znpcsplus.entity.properties.villager.VillagerProfessionProperty; +import lol.pyr.znpcsplus.entity.properties.villager.VillagerTypeProperty; +import lol.pyr.znpcsplus.entity.serializers.*; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.util.*; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Color; +import org.bukkit.DyeColor; +import org.bukkit.Sound; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 1.8 ... + * 1.9 ... + * 1.10 ... + * 1.11 ... + * 1.12 ... + * 1.13 ... + * 1.14 ... + * 1.15 ... + * 1.16 ... + * 1.17 ... + * 1.18-1.19 ... + * 1.20 ... + * 1.21 ... + */ +@SuppressWarnings("unchecked") +public class EntityPropertyRegistryImpl implements EntityPropertyRegistry { + private final Map, PropertySerializer> serializerMap = new HashMap<>(); + private final Map> byName = new HashMap<>(); + private final ConfigManager configManager; + + public EntityPropertyRegistryImpl(MojangSkinCache skinCache, ConfigManager configManager) { + registerSerializer(new ComponentPropertySerializer()); + registerSerializer(new NamedColorPropertySerializer()); + registerSerializer(new SkinDescriptorSerializer(skinCache)); + registerSerializer(new ItemStackPropertySerializer()); + registerSerializer(new ColorPropertySerializer()); + registerSerializer(new Vector3fPropertySerializer()); + registerSerializer(new BlockStatePropertySerializer()); + registerSerializer(new LookTypeSerializer()); + registerSerializer(new GenericSerializer<>(Vector3i::toString, Vector3i::fromString, Vector3i.class)); + + registerEnumSerializer(NpcPose.class); + registerEnumSerializer(DyeColor.class); + registerEnumSerializer(CatVariant.class); + registerEnumSerializer(CreeperState.class); + registerEnumSerializer(ParrotVariant.class); + registerEnumSerializer(SpellType.class); + registerEnumSerializer(FoxVariant.class); + registerEnumSerializer(FrogVariant.class); + registerEnumSerializer(VillagerType.class); + registerEnumSerializer(VillagerProfession.class); + registerEnumSerializer(VillagerLevel.class); + registerEnumSerializer(AxolotlVariant.class); + registerEnumSerializer(HorseType.class); + registerEnumSerializer(HorseColor.class); + registerEnumSerializer(HorseStyle.class); + registerEnumSerializer(HorseArmor.class); + registerEnumSerializer(LlamaVariant.class); + registerEnumSerializer(MooshroomVariant.class); + registerEnumSerializer(OcelotType.class); + registerEnumSerializer(PandaGene.class); + registerEnumSerializer(PuffState.class); + registerEnumSerializer(TropicalFishVariant.TropicalFishPattern.class); + registerEnumSerializer(SnifferState.class); + registerEnumSerializer(RabbitType.class); + registerEnumSerializer(AttachDirection.class); + registerEnumSerializer(Sound.class); + registerEnumSerializer(ArmadilloState.class); + registerEnumSerializer(WoldVariant.class); + registerEnumSerializer(SkeletonType.class); + + registerPrimitiveSerializers(Integer.class, Boolean.class, Double.class, Float.class, Long.class, Short.class, Byte.class, String.class); + + this.configManager = configManager; + + /* + registerType("using_item", false); // TODO: fix it for 1.8 and add new property to use offhand item and riptide animation + + // Enderman + registerType("enderman_held_block", new BlockState(0)); // TODO: figure out the type on this + registerType("enderman_screaming", false); // TODO + registerType("enderman_staring", false); // TODO + */ + } + + public void registerTypes(PacketFactory packetFactory, LegacyComponentSerializer textSerializer, TaskScheduler taskScheduler) { + ServerVersion ver = PacketEvents.getAPI().getServerManager().getVersion(); + boolean legacyBooleans = ver.isOlderThan(ServerVersion.V_1_9); + boolean legacyNames = ver.isOlderThan(ServerVersion.V_1_9); + boolean optionalComponents = ver.isNewerThanOrEquals(ServerVersion.V_1_13); + + register(new EquipmentProperty(packetFactory, "helmet", EquipmentSlot.HELMET)); + register(new EquipmentProperty(packetFactory, "chestplate", EquipmentSlot.CHEST_PLATE)); + register(new EquipmentProperty(packetFactory, "leggings", EquipmentSlot.LEGGINGS)); + register(new EquipmentProperty(packetFactory, "boots", EquipmentSlot.BOOTS)); + register(new EquipmentProperty(packetFactory, "hand", EquipmentSlot.MAIN_HAND)); + register(new EquipmentProperty(packetFactory, "offhand", EquipmentSlot.OFF_HAND)); + + register(new NameProperty(textSerializer, legacyNames, optionalComponents)); + register(new DummyProperty<>("display_name", String.class)); + register(new DinnerboneProperty(legacyNames, optionalComponents)); + + register(new DummyProperty<>("look", LookType.FIXED)); + register(new DummyProperty<>("look_distance", configManager.getConfig().lookPropertyDistance())); + register(new DummyProperty<>("look_return", false)); + register(new DummyProperty<>("view_distance", configManager.getConfig().viewDistance())); + + register(new DummyProperty<>("permission_required", false)); + + register(new ForceBodyRotationProperty(taskScheduler)); + + register(new DummyProperty<>("player_knockback", false)); + register(new DummyProperty<>("player_knockback_exempt_permission", String.class)); + register(new DummyProperty<>("player_knockback_distance", 0.4)); + register(new DummyProperty<>("player_knockback_vertical", 0.4)); + register(new DummyProperty<>("player_knockback_horizontal", 0.9)); + register(new DummyProperty<>("player_knockback_cooldown", 1500)); + register(new DummyProperty<>("player_knockback_sound", false)); + register(new DummyProperty<>("player_knockback_sound_volume", 1.0f)); + register(new DummyProperty<>("player_knockback_sound_pitch", 1.0f)); + register(new DummyProperty<>("player_knockback_sound_name", Sound.valueOf( + PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_9) ? + "VILLAGER_NO" : "ENTITY_VILLAGER_NO" + ))); + + register(new GlowProperty(packetFactory)); + register(new BitsetProperty("fire", 0, 0x01)); + register(new BitsetProperty("invisible", 0, 0x20)); + register(new HologramItemProperty()); + linkProperties("glow", "fire", "invisible"); + register(new BooleanProperty("silent", 4, false, legacyBooleans)); + + // Attribute Max Health + register(new AttributeProperty(packetFactory, "attribute_max_health", Attributes.MAX_HEALTH)); + + // Health - LivingEntity + int healthIndex = 6; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) healthIndex = 9; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) healthIndex = 8; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) healthIndex = 7; + register(new HealthProperty(healthIndex)); + + final int tameableIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) tameableIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) tameableIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) tameableIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) tameableIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) tameableIndex = 12; + else tameableIndex = 16; + register(new BitsetProperty("sitting", tameableIndex, 0x01)); + register(new BitsetProperty("tamed", tameableIndex, 0x04)); + + int potionIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) potionIndex = 10; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) potionIndex = 9; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) potionIndex = 8; + else potionIndex = 7; + register(new EncodedIntegerProperty<>("potion_color", Color.class, potionIndex++, Color::asRGB)); + register(new BooleanProperty("potion_ambient", potionIndex, false, legacyBooleans)); + + int babyIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) babyIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) babyIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) babyIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) babyIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) babyIndex = 11; + else babyIndex = 12; + if (ver.isOlderThan(ServerVersion.V_1_9)) { + register(new LegacyBabyProperty(babyIndex)); + } else { + register(new BooleanProperty("baby", babyIndex, false, legacyBooleans)); + } + + register(new EntitySittingProperty(packetFactory, this)); + + // Player + register(new DummyProperty<>("skin", SkinDescriptor.class, false)); + final int skinLayersIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) skinLayersIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_16)) skinLayersIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) skinLayersIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) skinLayersIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) skinLayersIndex = 12; + else skinLayersIndex = 10; + register(new BitsetProperty("skin_cape", skinLayersIndex, 0x01)); + register(new BitsetProperty("skin_jacket", skinLayersIndex, 0x02)); + register(new BitsetProperty("skin_left_sleeve", skinLayersIndex, 0x04)); + register(new BitsetProperty("skin_right_sleeve", skinLayersIndex, 0x08)); + register(new BitsetProperty("skin_left_leg", skinLayersIndex, 0x10)); + register(new BitsetProperty("skin_right_leg", skinLayersIndex, 0x20)); + register(new BitsetProperty("skin_hat", skinLayersIndex, 0x40)); + linkProperties("skin_cape", "skin_jacket", "skin_left_sleeve", "skin_right_sleeve", "skin_left_leg", "skin_right_leg", "skin_hat"); + + // Armor Stand + int armorStandIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) armorStandIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) armorStandIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) armorStandIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) armorStandIndex = 11; + else armorStandIndex = 10; + register(new BitsetProperty("small", armorStandIndex, 0x01)); + register(new BitsetProperty("arms", armorStandIndex, 0x04)); + register(new BitsetProperty("base_plate", armorStandIndex++, 0x08, true)); + linkProperties("small", "arms", "base_plate"); + register(new RotationProperty("head_rotation", armorStandIndex++, Vector3f.zero())); + register(new RotationProperty("body_rotation", armorStandIndex++, Vector3f.zero())); + register(new RotationProperty("left_arm_rotation", armorStandIndex++, new Vector3f(-10, 0, -10))); + register(new RotationProperty("right_arm_rotation", armorStandIndex++, new Vector3f(-15, 0, 10))); + register(new RotationProperty("left_leg_rotation", armorStandIndex++, new Vector3f(-1, 0, -1))); + register(new RotationProperty("right_leg_rotation", armorStandIndex, new Vector3f(1, 0, 1))); + + // Ghast + final int ghastAttackingIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) ghastAttackingIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) ghastAttackingIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) ghastAttackingIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) ghastAttackingIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) ghastAttackingIndex = 11; + else ghastAttackingIndex = 16; + register(new BooleanProperty("attacking", ghastAttackingIndex, false, legacyBooleans)); + + // Bat + final int batIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) batIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) batIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) batIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) batIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) batIndex = 11; + else batIndex = 16; + register(new BooleanProperty("hanging", batIndex, false, true /* This isn't a mistake */)); + + // Blaze + final int blazeIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) blazeIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) blazeIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) blazeIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) blazeIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) blazeIndex = 11; + else blazeIndex = 16; + register(new BitsetProperty("blaze_on_fire", blazeIndex, 0x01)); + + // Creeper + int creeperIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) creeperIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) creeperIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) creeperIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) creeperIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) creeperIndex = 11; + else creeperIndex= 16; + register(new EncodedIntegerProperty<>("creeper_state", CreeperState.IDLE, creeperIndex++, CreeperState::getState)); + register(new BooleanProperty("creeper_charged", creeperIndex, false, legacyBooleans)); + + // Abstract Horse + int horseIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) horseIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) horseIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) horseIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) horseIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) horseIndex = 12; + else horseIndex = 16; + int horseEating = ver.isNewerThanOrEquals(ServerVersion.V_1_12) ? 0x10 : 0x20; + register(new BitsetProperty("is_tame", horseIndex, 0x02, false, legacyBooleans)); + if (ver.isOlderThan(ServerVersion.V_1_21)) { + register(new BitsetProperty("is_saddled", horseIndex, 0x04, false, legacyBooleans)); + } + register(new BitsetProperty("is_eating", horseIndex, horseEating, false, legacyBooleans)); + register(new BitsetProperty("is_rearing", horseIndex, horseEating << 1, false, legacyBooleans)); + register(new BitsetProperty("has_mouth_open", horseIndex, horseEating << 2, false, legacyBooleans)); + + // End Crystal + if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) { + int endCrystalIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) endCrystalIndex = 8; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) endCrystalIndex = 7; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) endCrystalIndex = 6; + else endCrystalIndex = 5; + register(new OptionalBlockPosProperty("beam_target", null, endCrystalIndex++)); + register(new BooleanProperty("show_base", endCrystalIndex, true, false)); + } + + // Guardian + if (ver.isOlderThan(ServerVersion.V_1_11)) { + int guardianIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) guardianIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) guardianIndex = 11; + else guardianIndex = 16; + register(new BitsetProperty("is_elder", guardianIndex, 0x04, false, legacyBooleans)); + register(new BitsetProperty("is_retracting_spikes", guardianIndex, 0x02, false, legacyBooleans)); + linkProperties("is_elder", "is_retracting_spikes"); + // TODO: add guardian beam target + } else { + int guardianIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) guardianIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) guardianIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) guardianIndex = 14; + else guardianIndex = 12; + register(new BooleanProperty("is_retracting_spikes", guardianIndex, false, false)); + } + + // Horse + if (ver.isNewerThanOrEquals(ServerVersion.V_1_8) && ver.isOlderThan(ServerVersion.V_1_9)) { + register(new EncodedByteProperty<>("horse_type", HorseType.HORSE, 19, obj -> (byte) obj.ordinal())); + } else if (ver.isOlderThan(ServerVersion.V_1_11)) { + int horseTypeIndex = 14; + if (ver.isOlderThan(ServerVersion.V_1_10)) horseTypeIndex = 13; + register(new EncodedIntegerProperty<>("horse_type", HorseType.HORSE, horseTypeIndex, Enum::ordinal)); + } + int horseVariantIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_18)) horseVariantIndex = 18; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) horseVariantIndex = 19; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) horseVariantIndex = 18; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) horseVariantIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) horseVariantIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) horseVariantIndex = 14; + else horseVariantIndex = 20; + register(new HorseStyleProperty(horseVariantIndex)); + register(new HorseColorProperty(horseVariantIndex)); + linkProperties("horse_style", "horse_color"); + + // Use chesteplate property for 1.14 and above + if (ver.isOlderThan(ServerVersion.V_1_14)) { + int horseArmorIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_11)) horseArmorIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) horseArmorIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) horseArmorIndex = 16; + else horseArmorIndex = 22; + register(new EncodedIntegerProperty<>("horse_armor", HorseArmor.NONE, horseArmorIndex, Enum::ordinal)); + } + + // Chested Horse + if (ver.isOlderThan(ServerVersion.V_1_11)) { + register(new BitsetProperty("has_chest", horseIndex, 0x08, false, legacyBooleans)); + linkProperties("is_tame", "is_saddled", "has_chest", "is_eating", "is_rearing", "has_mouth_open"); + } else { + register(new BooleanProperty("has_chest", horseVariantIndex, false, legacyBooleans)); + if (ver.isOlderThan(ServerVersion.V_1_21)){ + linkProperties("is_tame", "is_saddled", "is_eating", "is_rearing", "has_mouth_open"); + } else { + linkProperties("is_tame", "is_eating", "is_rearing", "has_mouth_open"); + } + } + + // Slime, Magma Cube and Phantom + int sizeIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) sizeIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) sizeIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) sizeIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) sizeIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) sizeIndex = 11; + else sizeIndex = 16; + register(new IntegerProperty("size", sizeIndex, 1, legacyBooleans)); + + // Ocelot + if (ver.isOlderThan(ServerVersion.V_1_14)) { + int ocelotIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) ocelotIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) ocelotIndex = 14; + else ocelotIndex = 18; + if (legacyBooleans) register(new EncodedByteProperty<>("ocelot_type", OcelotType.OCELOT, ocelotIndex, obj -> (byte) obj.ordinal())); + else register(new EncodedIntegerProperty<>("ocelot_type", OcelotType.OCELOT, ocelotIndex, Enum::ordinal)); + } + + // Pig + int pigIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) pigIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) pigIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) pigIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) pigIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) pigIndex = 12; + else pigIndex = 16; + register(new BooleanProperty("pig_saddled", pigIndex, false, legacyBooleans)); + + // Rabbit + int rabbitIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) rabbitIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) rabbitIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) rabbitIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) rabbitIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) rabbitIndex = 12; + else rabbitIndex = 18; + register(new RabbitTypeProperty(rabbitIndex, legacyBooleans, legacyNames, optionalComponents)); + + // Sheep + int sheepIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) sheepIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) sheepIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) sheepIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) sheepIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) sheepIndex = 12; + else sheepIndex = 16; + // noinspection deprecation + register(new EncodedByteProperty<>("sheep_color", DyeColor.WHITE, sheepIndex, DyeColor::getWoolData)); + register(new BitsetProperty("sheep_sheared", sheepIndex, 0x10, false, legacyBooleans)); // no need to link because sheep_sheared is only visible when sheep_color is WHITE + + // Villager + int villagerIndex; + if (ver.isOlderThan(ServerVersion.V_1_14)) { + if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) villagerIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) villagerIndex = 12; + else villagerIndex = 16; + register(new EncodedIntegerProperty<>("villager_profession", VillagerProfession.NONE, villagerIndex, VillagerProfession::getLegacyId)); + } + + // Wolf + int wolfIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) wolfIndex = 19; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) wolfIndex = 18; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) wolfIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) wolfIndex = 15; + else wolfIndex = 19; + register(new BooleanProperty("wolf_begging", wolfIndex++, false, legacyBooleans)); + if (legacyBooleans) { + // noinspection deprecation + register(new EncodedByteProperty<>("wolf_collar", DyeColor.BLUE, wolfIndex++, DyeColor::getDyeData)); + } else register(new EncodedIntegerProperty<>("wolf_collar", DyeColor.RED, wolfIndex++, Enum::ordinal)); + if (ver.isNewerThanOrEquals(ServerVersion.V_1_16)) { + register(new EncodedIntegerProperty<>("wolf_angry", false, wolfIndex++, b -> b ? 1 : 0)); + linkProperties("tamed", "sitting"); + } + else { + register(new BitsetProperty("wolf_angry", tameableIndex, 0x02)); + linkProperties("wolf_angry", "tamed", "sitting"); + } + + // Wither + int witherIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) witherIndex = 16; // using the first index, so we can add the other properties later if needed + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) witherIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) witherIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) witherIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) witherIndex = 11; + else witherIndex = 17; + witherIndex += 3; // skip the first 3 indexes, will be used for the other properties later + register(new IntegerProperty("invulnerable_time", witherIndex, 0, false)); + + // Skeleton + if (ver.isOlderThan(ServerVersion.V_1_11)) { + if (legacyBooleans) register(new EncodedByteProperty<>("skeleton_type", SkeletonType.NORMAL, 13, SkeletonType::getLegacyId)); + else register(new EncodedIntegerProperty<>("skeleton_type", SkeletonType.NORMAL, ver.isOlderThan(ServerVersion.V_1_10) ? 11 : 12, Enum::ordinal)); + } + + // Zombie + int zombieIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) zombieIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_16)) zombieIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) zombieIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) zombieIndex = 12; + else zombieIndex = 13; + + if (ver.isOlderThan(ServerVersion.V_1_9)) { + register(new EncodedByteProperty<>("zombie_is_villager", false, zombieIndex++, b -> (byte) (b ? 1 : 0))); + } else if (ver.isOlderThan(ServerVersion.V_1_11)) { + register(new EncodedIntegerProperty<>("zombie_type", ZombieType.ZOMBIE, zombieIndex++, Enum::ordinal)); + } else { + zombieIndex++; // Not a mistake, this is field unused in 1.11+ + } + if (ver.isOlderThan(ServerVersion.V_1_9)) { + register(new EncodedByteProperty<>("is_converting", false, zombieIndex++, b -> (byte) (b ? 1 : 0))); + } else if (ver.isOlderThan(ServerVersion.V_1_11)) { + register(new BooleanProperty("is_converting", zombieIndex++, false, legacyBooleans)); + } + if (ver.isNewerThanOrEquals(ServerVersion.V_1_9) && ver.isOlderThan(ServerVersion.V_1_14)) { + register(new BooleanProperty("zombie_hands_held_up", zombieIndex++, false, legacyBooleans)); + } + if (ver.isNewerThanOrEquals(ServerVersion.V_1_13)) { + register(new BooleanProperty("zombie_becoming_drowned", zombieIndex++, false, legacyBooleans)); + } + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_9)) return; + // Shulker + int shulkerIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) shulkerIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) shulkerIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) shulkerIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) shulkerIndex = 12; + else shulkerIndex = 11; + register(new CustomTypeProperty<>("attach_direction", shulkerIndex++, AttachDirection.DOWN, EntityDataTypes.BLOCK_FACE, attachDir -> BlockFace.valueOf(attachDir.name()))); + register(new EncodedByteProperty<>("shield_height", 0, shulkerIndex++, value -> (byte) Math.max(0, Math.min(100, value)))); + // noinspection deprecation + register(new EncodedByteProperty<>("shulker_color", DyeColor.class, shulkerIndex, DyeColor::getWoolData)); + + // Snow Golem + int snowGolemIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) snowGolemIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) snowGolemIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) snowGolemIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) snowGolemIndex = 12; + else snowGolemIndex = 10; + register(new CustomTypeProperty<>("derpy_snowgolem", snowGolemIndex, false, EntityDataTypes.BYTE, b -> (byte) (b ? 0x00 : 0x10))); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_10)) return; + // Polar Bear + int polarBearIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) polarBearIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) polarBearIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) polarBearIndex = 15; + else polarBearIndex = 13; + register(new BooleanProperty("polar_bear_standing", polarBearIndex, false, false)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_11)) return; + // Spellcaster Illager + int spellIndex = 12; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) spellIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) spellIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) spellIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_12)) spellIndex = 13; + register(new EncodedByteProperty<>("spell", SpellType.NONE, spellIndex, obj -> (byte) Math.min(obj.ordinal(), ver.isOlderThan(ServerVersion.V_1_13) ? 3 : 5))); + + // Llama + int llamaIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_18)) llamaIndex = 20; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) llamaIndex = 21; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) llamaIndex = 20; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) llamaIndex = 19; + else llamaIndex = 17; + + // Removed in 1.21 + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_21)) register(new EncodedIntegerProperty("carpet_color", DyeColor.class, llamaIndex++, obj -> obj == null ? -1 : obj.ordinal())); + register(new EncodedIntegerProperty<>("llama_variant", LlamaVariant.CREAMY, llamaIndex, Enum::ordinal)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_12)) return; + // Parrot + int parrotIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) parrotIndex = 19; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) parrotIndex = 18; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) parrotIndex = 17; + else parrotIndex = 15; + register(new EncodedIntegerProperty<>("parrot_variant", ParrotVariant.RED_BLUE, parrotIndex, Enum::ordinal)); + + // Player + NBTProperty.NBTDecoder parrotVariantDecoder = (variant) -> { + NBTCompound compound = new NBTCompound(); + if (variant == null) return compound; + compound.setTag("id", new NBTString("minecraft:parrot")); + compound.setTag("Variant", new NBTInt(variant.ordinal())); + return compound; + }; + int shoulderIndex = skinLayersIndex+2; + register(new NBTProperty<>("shoulder_entity_left", ParrotVariant.class, shoulderIndex++, parrotVariantDecoder, true)); + register(new NBTProperty<>("shoulder_entity_right", ParrotVariant.class, shoulderIndex, parrotVariantDecoder, true)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_13)) return; + // Pufferfish + int pufferfishIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) pufferfishIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) pufferfishIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) pufferfishIndex = 15; + else pufferfishIndex = 13; + register(new EncodedIntegerProperty<>("puff_state", PuffState.DEFLATED, pufferfishIndex, Enum::ordinal)); + + // Tropical Fish + int tropicalFishIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) tropicalFishIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) tropicalFishIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) tropicalFishIndex = 15; + else tropicalFishIndex = 13; + register(new TropicalFishVariantProperty<>("tropical_fish_pattern", TropicalFishVariant.TropicalFishPattern.KOB, tropicalFishIndex, TropicalFishVariant.Builder::pattern)); + register(new TropicalFishVariantProperty<>("tropical_fish_body_color", DyeColor.WHITE, tropicalFishIndex, TropicalFishVariant.Builder::bodyColor)); + register(new TropicalFishVariantProperty<>("tropical_fish_pattern_color", DyeColor.WHITE, tropicalFishIndex, TropicalFishVariant.Builder::patternColor)); + linkProperties("tropical_fish_pattern", "tropical_fish_body_color", "tropical_fish_pattern_color"); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_14)) return; + // Pose + register(new CustomTypeProperty<>("pose", 6, NpcPose.STANDING, EntityDataTypes.ENTITY_POSE, npcPose -> EntityPose.valueOf(npcPose.name()))); + + // Villager + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) villagerIndex = 18; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) villagerIndex = 17; + else villagerIndex = 16; + register(new VillagerTypeProperty("villager_type", villagerIndex, VillagerType.PLAINS)); + register(new VillagerProfessionProperty("villager_profession", villagerIndex, VillagerProfession.NONE)); + register(new VillagerLevelProperty("villager_level", villagerIndex, VillagerLevel.STONE)); + linkProperties("villager_type", "villager_profession", "villager_level"); + + // Cat + int catIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) catIndex = 19; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) catIndex = 18; + else catIndex = 17; + register(new EncodedIntegerProperty<>("cat_variant", CatVariant.BLACK, catIndex++, Enum::ordinal, EntityDataTypes.CAT_VARIANT)); + register(new BooleanProperty("cat_laying", catIndex++, false, legacyBooleans)); + register(new BooleanProperty("cat_relaxed", catIndex++, false, legacyBooleans)); + register(new EncodedIntegerProperty<>("cat_collar", DyeColor.RED, catIndex, Enum::ordinal)); + + // Fox + int foxIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) foxIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) foxIndex = 16; + else foxIndex = 15; + register(new EncodedIntegerProperty<>("fox_variant", FoxVariant.RED, foxIndex++, Enum::ordinal)); + register(new BitsetProperty("fox_sitting", foxIndex, 0x01)); + register(new BitsetProperty("fox_crouching", foxIndex, 0x04)); + register(new BitsetProperty("fox_sleeping", foxIndex, 0x20)); + linkProperties("fox_sitting", "fox_crouching", "fox_sleeping"); + + int mooshroomIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) mooshroomIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) mooshroomIndex = 16; + else mooshroomIndex = 15; + register(new EncodedStringProperty<>("mooshroom_variant", MooshroomVariant.RED, mooshroomIndex, MooshroomVariant::getVariantName)); + + // Panda + int pandaIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) pandaIndex = 20; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) pandaIndex = 19; + else pandaIndex = 18; + register(new EncodedByteProperty<>("panda_main_gene", PandaGene.NORMAL, pandaIndex++, obj -> (byte) obj.ordinal())); + register(new EncodedByteProperty<>("panda_hidden_gene", PandaGene.NORMAL, pandaIndex++, obj -> (byte) obj.ordinal())); + if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) { + register(new BitsetProperty("panda_sneezing", pandaIndex, 0x02)); + register(new BitsetProperty("panda_rolling", pandaIndex, 0x04)); + register(new BitsetProperty("panda_sitting", pandaIndex, 0x08)); + register(new BitsetProperty("panda_on_back", pandaIndex, 0x10)); + linkProperties("panda_sneezing", "panda_rolling", "panda_sitting", "panda_on_back"); + } else { + register(new BitsetProperty("panda_sneezing", pandaIndex, 0x02)); + register(new BitsetProperty("panda_eating", pandaIndex, 0x04)); + linkProperties("panda_sneezing", "panda_eating"); + } + + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_15)) return; + + register(new BitsetProperty("fox_faceplanted", foxIndex, 0x40)); + linkProperties("fox_sitting", "fox_crouching", "fox_sleeping", "fox_faceplanted"); + + // Bee + int beeIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) beeIndex = 17; + else beeIndex = 18; + register(new BitsetProperty("has_nectar", beeIndex++, 0x08)); + register(new EncodedIntegerProperty<>("angry", false, beeIndex, enabled -> enabled ? 1 : 0)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_16)) return; + + // Hoglin and Piglin Zombification + final int zombificationIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) zombificationIndex = 17; // Change piglinIndex, pillagerIndex, striderIndex and vindicatorIndex if you change this + else zombificationIndex = 16; + register(new BooleanProperty("hoglin_immune_to_zombification", zombificationIndex, false, legacyBooleans)); + register(new BooleanProperty("piglin_immune_to_zombification", zombificationIndex-1, false, legacyBooleans)); + + // Piglin + int piglinIndex = zombificationIndex; + register(new BooleanProperty("piglin_baby", piglinIndex++, false, legacyBooleans)); + register(new BooleanProperty("piglin_charging_crossbow", piglinIndex++, false, legacyBooleans)); + register(new BooleanProperty("piglin_dancing", piglinIndex, false, legacyBooleans)); + + // Pillager + register(new BooleanProperty("pillager_charging", zombificationIndex, false, legacyBooleans)); + + // Strider + int striderIndex = zombificationIndex + 1; + register(new BooleanProperty("strider_shaking", striderIndex++, false, legacyBooleans)); // TODO: Fix this, it needs to be set constantly i guess + register(new BooleanProperty("strider_saddled", striderIndex, false, legacyBooleans)); + + // Vindicator + int vindicatorIndex = zombificationIndex -1; + register(new BooleanProperty("celebrating", vindicatorIndex, false, legacyBooleans)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_17)) return; + // Axolotl + register(new EncodedIntegerProperty<>("axolotl_variant", AxolotlVariant.LUCY, 17, Enum::ordinal)); + register(new BooleanProperty("playing_dead", 18, false, legacyBooleans)); + + // Goat + register(new BooleanProperty("has_left_horn", 18, true, legacyBooleans)); + register(new BooleanProperty("has_right_horn", 19, true, legacyBooleans)); + + register(new EncodedIntegerProperty<>("shaking", false,7, enabled -> enabled ? 140 : 0)); + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_19)) return; + // Frog + register(new EncodedIntegerProperty<>("frog_variant", FrogVariant.TEMPERATE, 17, Enum::ordinal, EntityDataTypes.FROG_VARIANT)); + + // Warden + register(new EncodedIntegerProperty<>("warden_anger", 0, 16, b -> Math.min(150, Math.max(0, b)))); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_20)) return; + + // Camel + int camelIndex = 18; + register(new BooleanProperty("bashing", camelIndex++, false, legacyBooleans)); + register(new CamelSittingProperty(6, camelIndex)); + + // Sniffer + register(new CustomTypeProperty<>("sniffer_state", 17, SnifferState.IDLING, EntityDataTypes.SNIFFER_STATE, state -> com.github.retrooper.packetevents.protocol.entity.sniffer.SnifferState.valueOf(state.name()))); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_20_5)) return; + // Armadillo + register(new CustomTypeProperty<>("armadillo_state", 17, ArmadilloState.IDLE, EntityDataTypes.ARMADILLO_STATE, state -> + com.github.retrooper.packetevents.protocol.entity.armadillo.ArmadilloState.valueOf(state.name()))); + + // Wolf + register(new EncodedIntegerProperty<>("wolf_variant", WoldVariant.PALE, wolfIndex, WoldVariant::getId, EntityDataTypes.WOLF_VARIANT)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_21)) return; + + register(new EquipmentProperty(packetFactory, "body", EquipmentSlot.BODY)); + + // Bogged + register(new BooleanProperty("bogged_sheared", 16, false, legacyBooleans)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_21_2)) return; + + // Creaking + register(new BooleanProperty("creaking_active", 17, false, legacyBooleans)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_21_4)) return; + + // Creaking + register(new BooleanProperty("creaking_crumbling", 18, false, legacyBooleans)); + } + + private void registerSerializer(PropertySerializer serializer) { + serializerMap.put(serializer.getTypeClass(), serializer); + } + + private > void registerEnumSerializer(Class clazz) { + serializerMap.put(clazz, new EnumPropertySerializer<>(clazz)); + } + + private void registerPrimitiveSerializers(Class... classes) { + for (Class clazz : classes) { + registerPrimitiveSerializer(clazz); + } + } + + private void registerPrimitiveSerializer(Class clazz) { + serializerMap.put(clazz, new PrimitivePropertySerializer<>(clazz)); + } + + private void register(EntityPropertyImpl property) { + if (byName.containsKey(property.getName())) + throw new IllegalArgumentException("Duplicate property name: " + property.getName()); + byName.put(property.getName(), property); + } + + private void linkProperties(String... names) { + linkProperties(Arrays.stream(names) + .map(this::getByName) + .collect(Collectors.toSet())); + } + + private void linkProperties(Collection> properties) { + for (EntityPropertyImpl property : properties) for (EntityPropertyImpl dependency : properties) { + if (property.equals(dependency)) continue; + property.addDependency(dependency); + } + } + + public PropertySerializer getSerializer(Class type) { + return (PropertySerializer) serializerMap.get(type); + } + + @Override + public Collection> getAll() { + return Collections.unmodifiableCollection( + byName.values().stream() + .map(property -> (EntityProperty) property) + .collect(Collectors.toSet())); + } + + public EntityPropertyImpl getByName(String name, Class type) { + return (EntityPropertyImpl) getByName(name); + } + + @Override + public void registerDummy(String name, Class type, boolean playerModifiable) { + register(new DummyProperty<>(name, type, playerModifiable)); + } + + public EntityPropertyImpl getByName(String name) { + return byName.get(name.toLowerCase()); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/EnumPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EnumPropertySerializer.java new file mode 100644 index 0000000..d71fb07 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EnumPropertySerializer.java @@ -0,0 +1,29 @@ +package lol.pyr.znpcsplus.entity; + +public class EnumPropertySerializer> implements PropertySerializer { + + private final Class enumClass; + + public EnumPropertySerializer(Class enumClass) { + this.enumClass = enumClass; + } + + @Override + public String serialize(T property) { + return property.name(); + } + + @Override + public T deserialize(String property) { + try { + return Enum.valueOf(enumClass, property.toUpperCase()); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Override + public Class getTypeClass() { + return enumClass; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/PacketEntity.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/PacketEntity.java new file mode 100644 index 0000000..e1cf15c --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/PacketEntity.java @@ -0,0 +1,224 @@ +package lol.pyr.znpcsplus.entity; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.reflection.Reflections; +import lol.pyr.znpcsplus.util.FutureUtil; +import lol.pyr.znpcsplus.util.NpcLocation; +import lol.pyr.znpcsplus.util.Viewable; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +public class PacketEntity implements PropertyHolder { + private final PacketFactory packetFactory; + + private final PropertyHolder properties; + private final Viewable viewable; + private final int entityId; + private final UUID uuid; + + private final EntityType type; + private NpcLocation location; + + private PacketEntity vehicle; + private Integer vehicleId; + private List passengers; + + public PacketEntity(PacketFactory packetFactory, PropertyHolder properties, Viewable viewable, EntityType type, NpcLocation location) { + this.packetFactory = packetFactory; + this.properties = properties; + this.viewable = viewable; + this.entityId = reserveEntityID(); + this.uuid = UUID.randomUUID(); + this.type = type; + this.location = location; + } + + public int getEntityId() { + return entityId; + } + + public NpcLocation getLocation() { + return location; + } + + public UUID getUuid() { + return uuid; + } + + public EntityType getType() { + return type; + } + + public void setLocation(NpcLocation location) { + this.location = location; + if (vehicle != null) { + vehicle.setLocation(location.withY(location.getY() - 0.9)); + return; + } + for (Player viewer : viewable.getViewers()) packetFactory.teleportEntity(viewer, this); + } + + public CompletableFuture spawn(Player player) { + return FutureUtil.exceptionPrintingRunAsync(() -> { + if (type == EntityTypes.PLAYER) packetFactory.spawnPlayer(player, this, properties).join(); + else packetFactory.spawnEntity(player, this, properties); + if (vehicle != null) { + setVehicle(vehicle); + } + if (vehicleId != null) { + packetFactory.setPassengers(player, vehicleId, this.getEntityId()); + } + if (passengers != null) { + packetFactory.setPassengers(player, this.getEntityId(), passengers.stream().mapToInt(Integer::intValue).toArray()); + } + }); + } + + public void setHeadRotation(Player player, float yaw, float pitch) { + packetFactory.sendHeadRotation(player, this, yaw, pitch); + } + + public PacketEntity getVehicle() { + return vehicle; + } + + public Viewable getViewable() { + return viewable; + } + + public void setVehicleId(Integer vehicleId) { + if (this.vehicle != null) { + for (Player player : viewable.getViewers()) { + packetFactory.setPassengers(player, this.vehicle.getEntityId()); + this.vehicle.despawn(player); + packetFactory.teleportEntity(player, this); + } + } else if (this.vehicleId != null) { + for (Player player : viewable.getViewers()) { + packetFactory.setPassengers(player, this.vehicleId); + } + } + this.vehicleId = vehicleId; + if (vehicleId == null) return; + + for (Player player : viewable.getViewers()) { + packetFactory.setPassengers(player, this.getEntityId(), vehicleId); + } + } + + public void setVehicle(PacketEntity vehicle) { + // remove old vehicle + if (this.vehicle != null) { + for (Player player : viewable.getViewers()) { + packetFactory.setPassengers(player, this.vehicle.getEntityId()); + this.vehicle.despawn(player); + packetFactory.teleportEntity(player, this); + } + } else if (this.vehicleId != null) { + for (Player player : viewable.getViewers()) { + packetFactory.setPassengers(player, this.vehicleId); + } + } + + this.vehicle = vehicle; + if (this.vehicle == null) return; + + vehicle.setLocation(location.withY(location.getY() - 0.9)); + for (Player player : viewable.getViewers()) { + vehicle.spawn(player).thenRun(() -> { + packetFactory.setPassengers(player, vehicle.getEntityId(), this.getEntityId()); + }); + } + } + + public Integer getVehicleId() { + return vehicleId; + } + + public List getPassengers() { + return passengers == null ? Collections.emptyList() : passengers; + } + + public void addPassenger(int entityId) { + if (passengers == null) { + passengers = new ArrayList<>(); + } + passengers.add(entityId); + for (Player player : viewable.getViewers()) { + packetFactory.setPassengers(player, this.getEntityId(), passengers.stream().mapToInt(Integer::intValue).toArray()); + } + } + + public void removePassenger(int entityId) { + if (passengers == null) return; + passengers.remove(entityId); + for (Player player : viewable.getViewers()) { + packetFactory.setPassengers(player, this.getEntityId(), passengers.stream().mapToInt(Integer::intValue).toArray()); + } + if (passengers.isEmpty()) { + passengers = null; + } + } + + public void despawn(Player player) { + packetFactory.destroyEntity(player, this, properties); + if (vehicle != null) vehicle.despawn(player); + } + + public void refreshMeta(Player player) { + packetFactory.sendAllMetadata(player, this, properties); + } + + public void swingHand(Player player, boolean offhand) { + packetFactory.sendHandSwing(player, this, offhand); + } + + private static int reserveEntityID() { + if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_14)) { + return Reflections.ATOMIC_ENTITY_ID_FIELD.get().incrementAndGet(); + } else { + int id = Reflections.ENTITY_ID_MODIFIER.get(); + Reflections.ENTITY_ID_MODIFIER.set(id + 1); + return id; + } + } + + @Override + public T getProperty(EntityProperty key) { + return properties.getProperty(key); + } + + @Override + public boolean hasProperty(EntityProperty key) { + return properties.hasProperty(key); + } + + @Override + public void setProperty(EntityProperty key, T value) { + properties.setProperty(key, value); + } + + @Override + public void setItemProperty(EntityProperty key, ItemStack value) { + properties.setItemProperty(key, value); + } + + @Override + public ItemStack getItemProperty(EntityProperty key) { + return properties.getItemProperty(key); + } + + @Override + public Set> getAppliedProperties() { + return properties.getAppliedProperties(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/ParrotNBTCompound.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/ParrotNBTCompound.java new file mode 100644 index 0000000..13eacf9 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/ParrotNBTCompound.java @@ -0,0 +1,21 @@ +package lol.pyr.znpcsplus.entity; + +import com.github.retrooper.packetevents.protocol.nbt.NBTCompound; +import com.github.retrooper.packetevents.protocol.nbt.NBTInt; +import com.github.retrooper.packetevents.protocol.nbt.NBTString; +import lol.pyr.znpcsplus.util.ParrotVariant; + +// Not sure where to put this or even if it's needed +public class ParrotNBTCompound { + private final NBTCompound tag = new NBTCompound(); + + public ParrotNBTCompound(ParrotVariant variant) { + tag.setTag("id", new NBTString("minecraft:parrot")); + tag.setTag("Variant", new NBTInt(variant.ordinal())); + // other tags if needed, idk + } + + public NBTCompound getTag() { + return tag; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/PrimitivePropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/PrimitivePropertySerializer.java new file mode 100644 index 0000000..ad18eaf --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/PrimitivePropertySerializer.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.entity; + +import java.lang.reflect.InvocationTargetException; + +public class PrimitivePropertySerializer implements PropertySerializer { + private final Class clazz; + + public PrimitivePropertySerializer(Class clazz) { + this.clazz = clazz; + } + + @Override + public String serialize(T property) { + return String.valueOf(property); + } + + @Override + public T deserialize(String property) { + try { + return clazz.getConstructor(String.class).newInstance(property); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) { + throw new NullPointerException("Failed to deserialize property " + property + " of type " + clazz.getName() + "!"); + } + } + + @Override + public Class getTypeClass() { + return clazz; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/PropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/PropertySerializer.java new file mode 100644 index 0000000..abd2f39 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/PropertySerializer.java @@ -0,0 +1,12 @@ +package lol.pyr.znpcsplus.entity; + +public interface PropertySerializer { + String serialize(T property); + T deserialize(String property); + Class getTypeClass(); + + @SuppressWarnings("unchecked") + default String UNSAFE_serialize(Object property) { + return serialize((T) property); + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/BitsetProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/BitsetProperty.java new file mode 100644 index 0000000..eb01483 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/BitsetProperty.java @@ -0,0 +1,52 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class BitsetProperty extends EntityPropertyImpl { + private final int index; + private final int bitmask; + private final boolean inverted; + private boolean integer = false; + + public BitsetProperty(String name, int index, int bitmask, boolean inverted, boolean integer) { + this(name, index, bitmask, inverted); + this.integer = integer; + } + + public BitsetProperty(String name, int index, int bitmask, boolean inverted) { + super(name, inverted, Boolean.class); + this.index = index; + this.bitmask = bitmask; + this.inverted = inverted; + } + + public BitsetProperty(String name, int index, int bitmask) { + this(name, index, bitmask, false); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + EntityData oldData = properties.get(index); + boolean enabled = entity.getProperty(this); + if (inverted) enabled = !enabled; + if (integer) { + int oldValue = 0; + if (oldData != null && oldData.getValue() instanceof Number) { + oldValue = ((Number) oldData.getValue()).intValue(); + } + properties.put(index, newEntityData(index, EntityDataTypes.INT, oldValue | (enabled ? bitmask : 0))); + } else { + byte oldValue = 0; + if (oldData != null && oldData.getValue() instanceof Number) { + oldValue = ((Number) oldData.getValue()).byteValue(); + } + properties.put(index, newEntityData(index, EntityDataTypes.BYTE, (byte) (oldValue | (enabled ? bitmask : 0)))); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/BooleanProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/BooleanProperty.java new file mode 100644 index 0000000..bddc0bf --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/BooleanProperty.java @@ -0,0 +1,34 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class BooleanProperty extends EntityPropertyImpl { + private final int index; + private final boolean legacy; + private final boolean inverted; + + public BooleanProperty(String name, int index, boolean defaultValue, boolean legacy) { + this(name, index, defaultValue, legacy, false); + } + + public BooleanProperty(String name, int index, boolean defaultValue, boolean legacy, boolean inverted) { + super(name, defaultValue, Boolean.class); + this.index = index; + this.legacy = legacy; + this.inverted = inverted; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + boolean enabled = entity.getProperty(this); + if (inverted) enabled = !enabled; + if (legacy) properties.put(index, newEntityData(index, EntityDataTypes.BYTE, (byte) (enabled ? 1 : 0))); + else properties.put(index, newEntityData(index, EntityDataTypes.BOOLEAN, enabled)); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/CamelSittingProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/CamelSittingProperty.java new file mode 100644 index 0000000..0afa31d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/CamelSittingProperty.java @@ -0,0 +1,33 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.entity.pose.EntityPose; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class CamelSittingProperty extends EntityPropertyImpl { + private final int poseIndex; + private final int lastPoseTickIndex; + + public CamelSittingProperty(int poseIndex, int lastPoseTickIndex) { + super("camel_sitting", false, Boolean.class); + this.poseIndex = poseIndex; + this.lastPoseTickIndex = lastPoseTickIndex; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + boolean value = entity.getProperty(this); + if (value) { + properties.put(poseIndex, newEntityData(poseIndex, EntityDataTypes.ENTITY_POSE, EntityPose.SITTING)); + properties.put(lastPoseTickIndex, newEntityData(lastPoseTickIndex, EntityDataTypes.LONG, -1L)); + } else { + properties.put(poseIndex, newEntityData(poseIndex, EntityDataTypes.ENTITY_POSE, EntityPose.STANDING)); + properties.put(lastPoseTickIndex, newEntityData(lastPoseTickIndex, EntityDataTypes.LONG, 0L)); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/CustomTypeProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/CustomTypeProperty.java new file mode 100644 index 0000000..0d142d2 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/CustomTypeProperty.java @@ -0,0 +1,32 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataType; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class CustomTypeProperty extends EntityPropertyImpl { + private final int index; + private final EntityDataType type; + private final TypeDecoder decoder; + + @SuppressWarnings("unchecked") + public CustomTypeProperty(String name, int index, T def, EntityDataType type, TypeDecoder decoder) { + super(name, def, (Class) def.getClass()); + this.index = index; + this.type = type; + this.decoder = decoder; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + properties.put(index, newEntityData(index, type, decoder.decode(entity.getProperty(this)))); + } + + public interface TypeDecoder { + U decode(T obj); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/DinnerboneProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/DinnerboneProperty.java new file mode 100644 index 0000000..9df06f1 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/DinnerboneProperty.java @@ -0,0 +1,35 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.util.adventure.AdventureSerializer; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.Optional; + +public class DinnerboneProperty extends EntityPropertyImpl { + private final boolean optional; + private final Object serialized; + + public DinnerboneProperty(boolean legacy, boolean optional) { + super("dinnerbone", false, Boolean.class); + this.optional = optional; + Component name = Component.text("Dinnerbone"); + this.serialized = legacy ? AdventureSerializer.serializer().legacy().serialize(name) : + optional ? name : LegacyComponentSerializer.legacySection().serialize(name); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + if (optional) { + properties.put(2, new EntityData<>(2, EntityDataTypes.OPTIONAL_ADV_COMPONENT, entity.getProperty(this) ? Optional.of((Component) serialized) : Optional.empty())); + } else { + properties.put(2, new EntityData<>(2, EntityDataTypes.STRING, entity.getProperty(this) ? (String) serialized : "")); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/DummyProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/DummyProperty.java new file mode 100644 index 0000000..78d3308 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/DummyProperty.java @@ -0,0 +1,33 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class DummyProperty extends EntityPropertyImpl { + public DummyProperty(String name, T defaultValue) { + this(name, defaultValue, true); + } + + public DummyProperty(String name, Class clazz) { + this(name, clazz, true); + } + + @SuppressWarnings("unchecked") + public DummyProperty(String name, T defaultValue, boolean playerModifiable) { + super(name, defaultValue, (Class) defaultValue.getClass()); + setPlayerModifiable(playerModifiable); + } + + public DummyProperty(String name, Class clazz, boolean playerModifiable) { + super(name, null, clazz); + setPlayerModifiable(playerModifiable); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedByteProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedByteProperty.java new file mode 100644 index 0000000..19fa125 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedByteProperty.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataType; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class EncodedByteProperty extends EntityPropertyImpl { + private final EntityDataType type; + private final ByteDecoder decoder; + private final int index; + + protected EncodedByteProperty(String name, T defaultValue, Class clazz, int index, ByteDecoder decoder, EntityDataType type) { + super(name, defaultValue, clazz); + this.decoder = decoder; + this.index = index; + this.type = type; + } + + @SuppressWarnings("unchecked") + public EncodedByteProperty(String name, T defaultValue, int index, ByteDecoder decoder) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, EntityDataTypes.BYTE); + } + + @SuppressWarnings("unchecked") + public EncodedByteProperty(String name, T defaultValue, int index, ByteDecoder decoder, EntityDataType type) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, type); + } + + public EncodedByteProperty(String name, Class clazz, int index, ByteDecoder decoder) { + this(name, null, clazz, index, decoder, EntityDataTypes.BYTE); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + T value = entity.getProperty(this); + if (value == null) return; + properties.put(index, newEntityData(index, type, decoder.decode(value))); + } + + public interface ByteDecoder { + byte decode(T obj); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedIntegerProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedIntegerProperty.java new file mode 100644 index 0000000..896faa1 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedIntegerProperty.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataType; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class EncodedIntegerProperty extends EntityPropertyImpl { + private final EntityDataType type; + private final IntegerDecoder decoder; + private final int index; + + protected EncodedIntegerProperty(String name, T defaultValue, Class clazz, int index, IntegerDecoder decoder, EntityDataType type) { + super(name, defaultValue, clazz); + this.decoder = decoder; + this.index = index; + this.type = type; + } + + @SuppressWarnings("unchecked") + public EncodedIntegerProperty(String name, T defaultValue, int index, IntegerDecoder decoder) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, EntityDataTypes.INT); + } + + @SuppressWarnings("unchecked") + public EncodedIntegerProperty(String name, T defaultValue, int index, IntegerDecoder decoder, EntityDataType type) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, type); + } + + public EncodedIntegerProperty(String name, Class clazz, int index, IntegerDecoder decoder) { + this(name, null, clazz, index, decoder, EntityDataTypes.INT); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + T value = entity.getProperty(this); + if (value == null) return; + properties.put(index, newEntityData(index, type, decoder.decode(value))); + } + + public interface IntegerDecoder { + int decode(T obj); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedStringProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedStringProperty.java new file mode 100644 index 0000000..2ebc088 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedStringProperty.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataType; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class EncodedStringProperty extends EntityPropertyImpl { + private final EntityDataType type; + private final EncodedStringProperty.StringDecoder decoder; + private final int index; + + public EncodedStringProperty(String name, T defaultValue, Class clazz, int index, StringDecoder decoder, EntityDataType type) { + super(name, defaultValue, clazz); + this.decoder = decoder; + this.index = index; + this.type = type; + } + + @SuppressWarnings("unchecked") + public EncodedStringProperty(String name, T defaultValue, int index, StringDecoder decoder) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, EntityDataTypes.STRING); + } + + @SuppressWarnings("unchecked") + public EncodedStringProperty(String name, T defaultValue, int index, StringDecoder decoder, EntityDataType type) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, type); + } + + public EncodedStringProperty(String name, Class clazz, int index, StringDecoder decoder) { + this(name, null, clazz, index, decoder, EntityDataTypes.STRING); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + T value = entity.getProperty(this); + if (value == null) return; + properties.put(index, newEntityData(index, type, decoder.decode(value))); + } + + public interface StringDecoder { + String decode(T obj); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EntitySittingProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EntitySittingProperty.java new file mode 100644 index 0000000..a3e6f82 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EntitySittingProperty.java @@ -0,0 +1,37 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import lol.pyr.znpcsplus.entity.ArmorStandVehicleProperties; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.packets.PacketFactory; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class EntitySittingProperty extends EntityPropertyImpl { + private final PacketFactory packetFactory; + private final EntityPropertyRegistryImpl propertyRegistry; + + public EntitySittingProperty(PacketFactory packetFactory, EntityPropertyRegistryImpl propertyRegistry) { + super("entity_sitting", false, Boolean.class); + this.packetFactory = packetFactory; + this.propertyRegistry = propertyRegistry; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + boolean sitting = entity.getProperty(this); + if (sitting) { + if (entity.getVehicle() == null) { + PacketEntity vehiclePacketEntity = new PacketEntity(packetFactory, new ArmorStandVehicleProperties(propertyRegistry), + entity.getViewable(), EntityTypes.ARMOR_STAND, entity.getLocation().withY(entity.getLocation().getY() - 0.9)); + entity.setVehicle(vehiclePacketEntity); + } + } else if (entity.getVehicle() != null) { + entity.setVehicle(null); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EquipmentProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EquipmentProperty.java new file mode 100644 index 0000000..1bef980 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EquipmentProperty.java @@ -0,0 +1,28 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import com.github.retrooper.packetevents.protocol.player.Equipment; +import com.github.retrooper.packetevents.protocol.player.EquipmentSlot; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.packets.PacketFactory; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class EquipmentProperty extends EntityPropertyImpl { + private final PacketFactory packetFactory; + private final EquipmentSlot slot; + + public EquipmentProperty(PacketFactory packetFactory, String name, EquipmentSlot slot) { + super(name, null, ItemStack.class); + this.packetFactory = packetFactory; + this.slot = slot; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + packetFactory.sendEquipment(player, entity, new Equipment(slot, entity.getProperty(this))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/ForceBodyRotationProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/ForceBodyRotationProperty.java new file mode 100644 index 0000000..56654b5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/ForceBodyRotationProperty.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class ForceBodyRotationProperty extends DummyProperty { + private final TaskScheduler scheduler; + + public ForceBodyRotationProperty(TaskScheduler scheduler) { + super("force_body_rotation", false); + this.scheduler = scheduler; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + if (entity.getProperty(this)) { + scheduler.runLaterAsync(() -> entity.swingHand(player, false), 2L); + scheduler.runLaterAsync(() -> entity.swingHand(player, false), 6L); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/GlowProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/GlowProperty.java new file mode 100644 index 0000000..87c15bf --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/GlowProperty.java @@ -0,0 +1,37 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.util.NamedColor; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class GlowProperty extends EntityPropertyImpl { + private final PacketFactory packetFactory; + + public GlowProperty(PacketFactory packetFactory) { + super("glow", null, NamedColor.class); + this.packetFactory = packetFactory; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + NamedColor value = entity.getProperty(this); + EntityData oldData = properties.get(0); +// byte oldValue = oldData == null ? 0 : (byte) oldData.getValue(); + byte oldValue = 0; + if (oldData != null && oldData.getValue() instanceof Number) { + oldValue = ((Number) oldData.getValue()).byteValue(); + } + properties.put(0, newEntityData(0, EntityDataTypes.BYTE, (byte) (oldValue | (value == null ? 0 : 0x40)))); + // the team is already created with the right glow color in the packet factory if the npc isnt spawned yet + if (isSpawned) { + packetFactory.removeTeam(player, entity); + packetFactory.createTeam(player, entity, value); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HealthProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HealthProperty.java new file mode 100644 index 0000000..0e6aede --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HealthProperty.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.attribute.Attributes; +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class HealthProperty extends EntityPropertyImpl { + private final int index; + + public HealthProperty(int index) { + super("health", 20f, Float.class); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + float health = entity.getProperty(this); + health = (float) Attributes.MAX_HEALTH.sanitizeValue(health); + properties.put(index, new EntityData<>(index, EntityDataTypes.FLOAT, health)); + + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HologramItemProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HologramItemProperty.java new file mode 100644 index 0000000..778a2ba --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HologramItemProperty.java @@ -0,0 +1,24 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class HologramItemProperty extends EntityPropertyImpl { + + public HologramItemProperty() { + super("holo_item", null, ItemStack.class); + setPlayerModifiable(false); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + properties.put(8, newEntityData(8, EntityDataTypes.ITEMSTACK, entity.getProperty(this))); + properties.put(5, newEntityData(5, EntityDataTypes.BOOLEAN, true)); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HorseColorProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HorseColorProperty.java new file mode 100644 index 0000000..fed043d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HorseColorProperty.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.HorseColor; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class HorseColorProperty extends EntityPropertyImpl { + private final int index; + + public HorseColorProperty(int index) { + super("horse_color", HorseColor.WHITE, HorseColor.class); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + EntityData oldData = properties.get(index); + HorseColor value = entity.getProperty(this); + int oldValue = (oldData != null && oldData.getValue() instanceof Integer) ? (Integer) oldData.getValue() : 0; + + int newValue = value.ordinal() | (oldValue & 0xFF00); + properties.put(index, newEntityData(index, EntityDataTypes.INT, newValue)); + + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HorseStyleProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HorseStyleProperty.java new file mode 100644 index 0000000..ce0acc3 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HorseStyleProperty.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.HorseStyle; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class HorseStyleProperty extends EntityPropertyImpl { + private final int index; + + public HorseStyleProperty(int index) { + super("horse_style", HorseStyle.NONE, HorseStyle.class); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + EntityData oldData = properties.get(index); + HorseStyle value = entity.getProperty(this); + + int oldValue = (oldData != null && oldData.getValue() instanceof Integer) ? (Integer) oldData.getValue() : 0; + int newValue = (oldValue & 0x00FF) | (value.ordinal() << 8); + properties.put(index, newEntityData(index, EntityDataTypes.INT, newValue)); + + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/IntegerProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/IntegerProperty.java new file mode 100644 index 0000000..47607ad --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/IntegerProperty.java @@ -0,0 +1,31 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class IntegerProperty extends EntityPropertyImpl { + private final int index; + private final boolean legacy; + + public IntegerProperty(String name, int index, Integer defaultValue) { + this(name, index, defaultValue, false); + } + + public IntegerProperty(String name, int index, Integer defaultValue, boolean legacy) { + super(name, defaultValue, Integer.class); + this.index = index; + this.legacy = legacy; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + properties.put(index, legacy ? + newEntityData(index, EntityDataTypes.BYTE, (byte) entity.getProperty(this).intValue()) : + newEntityData(index, EntityDataTypes.INT, entity.getProperty(this))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/LegacyBabyProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/LegacyBabyProperty.java new file mode 100644 index 0000000..3d9f0e7 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/LegacyBabyProperty.java @@ -0,0 +1,29 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class LegacyBabyProperty extends EntityPropertyImpl { + private final int index; + + public LegacyBabyProperty(int index) { + super("baby", false, Boolean.class); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + boolean isBaby = entity.getProperty(this); + if (entity.getType().equals(EntityTypes.ZOMBIE)) { + properties.put(index, newEntityData(index, EntityDataTypes.BYTE, (byte) (isBaby ? 1 : 0))); + } else { + properties.put(index, newEntityData(index, EntityDataTypes.BYTE, (byte) (isBaby ? -1 : 0))); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/NBTProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/NBTProperty.java new file mode 100644 index 0000000..28bd952 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/NBTProperty.java @@ -0,0 +1,60 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataType; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.nbt.NBTCompound; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class NBTProperty extends EntityPropertyImpl { + private final EntityDataType type; + private final NBTDecoder decoder; + private final int index; + private final boolean allowNull; // This means that the decoder can have null input, not that the property can be null + + public NBTProperty(String name, T defaultValue, Class clazz, int index, NBTDecoder decoder, boolean allowNull, EntityDataType type) { + super(name, defaultValue, clazz); + this.decoder = decoder; + this.index = index; + this.allowNull = allowNull; + this.type = type; + } + + @SuppressWarnings("unchecked") + public NBTProperty(String name, T defaultValue, int index, NBTDecoder decoder, boolean allowNull) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, allowNull, EntityDataTypes.NBT); + } + + @SuppressWarnings("unchecked") + public NBTProperty(String name, T defaultValue, int index, NBTDecoder decoder) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, false, EntityDataTypes.NBT); + } + + @SuppressWarnings("unchecked") + public NBTProperty(String name, T defaultValue, int index, NBTDecoder decoder, boolean allowNull, EntityDataType type) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, allowNull, type); + } + + public NBTProperty(String name, Class clazz, int index, NBTDecoder decoder, boolean allowNull) { + this(name, null, clazz, index, decoder, allowNull, EntityDataTypes.NBT); + } + + public NBTProperty(String name, Class clazz, int index, NBTDecoder decoder) { + this(name, null, clazz, index, decoder, false, EntityDataTypes.NBT); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + T value = entity.getProperty(this); + if (value == null && !allowNull) return; + properties.put(index, newEntityData(index, type, decoder.decode(value))); + } + + public interface NBTDecoder { + NBTCompound decode(T obj); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/NameProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/NameProperty.java new file mode 100644 index 0000000..d0e9400 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/NameProperty.java @@ -0,0 +1,46 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.util.adventure.AdventureSerializer; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.PapiUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.Optional; + +public class NameProperty extends EntityPropertyImpl { + private final LegacyComponentSerializer legacySerializer; + private final boolean legacySerialization; + private final boolean optional; + + public NameProperty(LegacyComponentSerializer legacySerializer, boolean legacySerialization, boolean optional) { + super("name", null, Component.class); + this.legacySerializer = legacySerializer; + + this.legacySerialization = legacySerialization; + this.optional = optional; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + Component value = entity.getProperty(this); + if (value != null) { + value = PapiUtil.set(legacySerializer, player, value); + if (legacySerialization) { + properties.put(2, newEntityData(2, EntityDataTypes.STRING, AdventureSerializer.serializer().asJson(value))); + } else if (optional) { + properties.put(2, newEntityData(2, EntityDataTypes.OPTIONAL_ADV_COMPONENT, Optional.of(value))); + } else { + properties.put(2, newEntityData(2, EntityDataTypes.STRING, LegacyComponentSerializer.legacySection().serialize(value))); + } + } + + if (legacySerialization) properties.put(3, newEntityData(3, EntityDataTypes.BYTE, (byte) (value != null ? 1 : 0))); + else properties.put(3, newEntityData(3, EntityDataTypes.BOOLEAN, value != null)); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/OptionalBlockPosProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/OptionalBlockPosProperty.java new file mode 100644 index 0000000..8b233b2 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/OptionalBlockPosProperty.java @@ -0,0 +1,28 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.Vector3i; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.Optional; + +public class OptionalBlockPosProperty extends EntityPropertyImpl { + private final int index; + + public OptionalBlockPosProperty(String name, Vector3i defaultValue, int index) { + super(name, defaultValue, Vector3i.class); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + Vector3i value = entity.getProperty(this); + if (value == null) properties.put(index, new EntityData<>(index, EntityDataTypes.OPTIONAL_BLOCK_POSITION, Optional.empty())); + else properties.put(index, new EntityData<>(index, EntityDataTypes.OPTIONAL_BLOCK_POSITION, + Optional.of(new com.github.retrooper.packetevents.util.Vector3i(value.getX(), value.getY(), value.getZ())))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/RabbitTypeProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/RabbitTypeProperty.java new file mode 100644 index 0000000..a5b6ead --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/RabbitTypeProperty.java @@ -0,0 +1,53 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.util.adventure.AdventureSerializer; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.RabbitType; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.Optional; + +public class RabbitTypeProperty extends EntityPropertyImpl { + private final int index; + private final boolean legacyBooleans; + private final boolean optional; + private final Object serialized; + + public RabbitTypeProperty(int index, boolean legacyBooleans, boolean legacyNames, boolean optional) { + super("rabbit_type", RabbitType.BROWN, RabbitType.class); + this.index = index; + this.legacyBooleans = legacyBooleans; + this.optional = optional; + Component name = Component.text("Toast"); + this.serialized = legacyNames ? AdventureSerializer.serializer().legacy().serialize(name) : + optional ? name : LegacyComponentSerializer.legacySection().serialize(name); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + RabbitType rabbitType = entity.getProperty(this); + if (rabbitType == null) return; + if (!rabbitType.equals(RabbitType.TOAST)) { + properties.put(index, legacyBooleans ? + newEntityData(index, EntityDataTypes.BYTE, (byte) rabbitType.getId()) : + newEntityData(index, EntityDataTypes.INT, rabbitType.getId())); + if (optional) { + properties.put(2, new EntityData<>(2, EntityDataTypes.OPTIONAL_ADV_COMPONENT, Optional.empty())); + } else { + properties.put(2, new EntityData<>(2, EntityDataTypes.STRING, "")); + } + } else { + if (optional) { + properties.put(2, newEntityData(2, EntityDataTypes.OPTIONAL_ADV_COMPONENT, Optional.of((Component) serialized))); + } else { + properties.put(2, newEntityData(2, EntityDataTypes.STRING, (String) serialized)); + } + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/RotationProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/RotationProperty.java new file mode 100644 index 0000000..2754a2d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/RotationProperty.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.Vector3f; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class RotationProperty extends EntityPropertyImpl { + private final int index; + + public RotationProperty(String name, int index, Vector3f defaultValue) { + super(name, defaultValue, Vector3f.class); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + Vector3f vec = entity.getProperty(this); + properties.put(index, newEntityData(index, EntityDataTypes.ROTATION, new com.github.retrooper.packetevents.util.Vector3f(vec.getX(), vec.getY(), vec.getZ()))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/TargetNpcProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/TargetNpcProperty.java new file mode 100644 index 0000000..5fc53c4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/TargetNpcProperty.java @@ -0,0 +1,29 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class TargetNpcProperty extends EntityPropertyImpl { + private final int index; + + public TargetNpcProperty(String name, int index, NpcEntryImpl defaultValue) { + super(name, defaultValue, NpcEntryImpl.class); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + NpcEntryImpl value = entity.getProperty(this); + if (value == null) return; + if (value.getNpc().getEntity().getEntityId() == entity.getEntityId()) return; + if (value.getNpc().isVisibleTo(player)) { + properties.put(index, newEntityData(index, EntityDataTypes.INT, value.getNpc().getEntity().getEntityId())); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/TropicalFishVariantProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/TropicalFishVariantProperty.java new file mode 100644 index 0000000..aa83c15 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/TropicalFishVariantProperty.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.TropicalFishVariant; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class TropicalFishVariantProperty extends EntityPropertyImpl { + private final int index; + private final BuilderDecoder decoder; + + public TropicalFishVariantProperty(String name, T defaultValue, Class type, int index, BuilderDecoder decoder) { + super(name, defaultValue, type); + this.index = index; + this.decoder = decoder; + } + + @SuppressWarnings("unchecked") + public TropicalFishVariantProperty(String name, T defaultValue, int index, BuilderDecoder decoder) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + T value = entity.getProperty(this); + if (value == null) return; + + EntityData oldData = properties.get(index); + TropicalFishVariant.Builder builder; + if (oldData != null && oldData.getType() == EntityDataTypes.INT && oldData.getValue() instanceof Integer) { + int oldVal = (Integer) oldData.getValue(); + builder = TropicalFishVariant.Builder.fromInt(oldVal); + } else { + builder = new TropicalFishVariant.Builder(); + } + builder = decoder.decode(builder, value); + int variant = builder.build().getVariant(); + properties.put(index, newEntityData(index, EntityDataTypes.INT, variant)); + } + + public interface BuilderDecoder { + TropicalFishVariant.Builder decode(TropicalFishVariant.Builder builder, T obj); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/attributes/AttributeProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/attributes/AttributeProperty.java new file mode 100644 index 0000000..e1aba38 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/attributes/AttributeProperty.java @@ -0,0 +1,63 @@ +package lol.pyr.znpcsplus.entity.properties.attributes; + +import com.github.retrooper.packetevents.protocol.attribute.Attribute; +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUpdateAttributes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.packets.PacketFactory; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class AttributeProperty extends EntityPropertyImpl { + private final PacketFactory packetFactory; + private final Attribute attribute; + + public AttributeProperty(PacketFactory packetFactory, String name, Attribute attribute) { + super(name, attribute.getDefaultValue(), Double.class); + this.packetFactory = packetFactory; + this.attribute = attribute; + } + + public double getMinValue() { + return attribute.getMinValue(); + } + + public double getMaxValue() { + return attribute.getMaxValue(); + } + + public double sanitizeValue(double value) { + return attribute.sanitizeValue(value); + } + + @Override + public List> applyStandalone(Player player, PacketEntity packetEntity, boolean isSpawned) { + apply(player, packetEntity, isSpawned, Collections.emptyList()); + return Collections.emptyList(); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + } + + public void apply(Player player, PacketEntity entity, boolean isSpawned, List properties) { + Double value = entity.getProperty(this); + if (value == null) { + return; + } + value = attribute.sanitizeValue(value); + if (isSpawned) { + packetFactory.sendAttribute(player, entity, new WrapperPlayServerUpdateAttributes.Property(attribute, value, Collections.emptyList())); + } else { + properties.add(new WrapperPlayServerUpdateAttributes.Property(attribute, value, Collections.emptyList())); + } + } + + public Attribute getAttribute() { + return attribute; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerDataProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerDataProperty.java new file mode 100644 index 0000000..36d55ad --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerDataProperty.java @@ -0,0 +1,31 @@ +package lol.pyr.znpcsplus.entity.properties.villager; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.entity.villager.VillagerData; +import com.github.retrooper.packetevents.protocol.entity.villager.profession.VillagerProfessions; +import com.github.retrooper.packetevents.protocol.entity.villager.type.VillagerTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public abstract class VillagerDataProperty extends EntityPropertyImpl { + private final int index; + + @SuppressWarnings("unchecked") + public VillagerDataProperty(String name, int index, T def) { + super(name, def, (Class) def.getClass()); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + EntityData oldData = properties.get(index); + VillagerData old = oldData == null ? new VillagerData(VillagerTypes.PLAINS, VillagerProfessions.NONE, 1) : (VillagerData) oldData.getValue(); + properties.put(index, newEntityData(index, EntityDataTypes.VILLAGER_DATA, apply(old, entity.getProperty(this)))); + } + + protected abstract VillagerData apply(VillagerData data, T value); +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerLevelProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerLevelProperty.java new file mode 100644 index 0000000..af81f40 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerLevelProperty.java @@ -0,0 +1,16 @@ +package lol.pyr.znpcsplus.entity.properties.villager; + +import com.github.retrooper.packetevents.protocol.entity.villager.VillagerData; +import lol.pyr.znpcsplus.util.VillagerLevel; + +public class VillagerLevelProperty extends VillagerDataProperty { + public VillagerLevelProperty(String name, int index, VillagerLevel def) { + super(name, index, def); + } + + @Override + protected VillagerData apply(VillagerData data, VillagerLevel value) { + data.setLevel(value.ordinal() + 1); + return data; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerProfessionProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerProfessionProperty.java new file mode 100644 index 0000000..6571676 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerProfessionProperty.java @@ -0,0 +1,17 @@ +package lol.pyr.znpcsplus.entity.properties.villager; + +import com.github.retrooper.packetevents.protocol.entity.villager.VillagerData; +import com.github.retrooper.packetevents.protocol.entity.villager.profession.VillagerProfessions; +import lol.pyr.znpcsplus.util.VillagerProfession; + +public class VillagerProfessionProperty extends VillagerDataProperty { + public VillagerProfessionProperty(String name, int index, VillagerProfession def) { + super(name, index, def); + } + + @Override + protected VillagerData apply(VillagerData data, VillagerProfession value) { + data.setProfession(VillagerProfessions.getById(value.getId())); + return data; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerTypeProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerTypeProperty.java new file mode 100644 index 0000000..a3d2448 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerTypeProperty.java @@ -0,0 +1,17 @@ +package lol.pyr.znpcsplus.entity.properties.villager; + +import com.github.retrooper.packetevents.protocol.entity.villager.VillagerData; +import com.github.retrooper.packetevents.protocol.entity.villager.type.VillagerTypes; +import lol.pyr.znpcsplus.util.VillagerType; + +public class VillagerTypeProperty extends VillagerDataProperty { + public VillagerTypeProperty(String name, int index, VillagerType def) { + super(name, index, def); + } + + @Override + protected VillagerData apply(VillagerData data, VillagerType value) { + data.setType(VillagerTypes.getById(value.getId())); + return data; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/BlockStatePropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/BlockStatePropertySerializer.java new file mode 100644 index 0000000..c29f9b2 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/BlockStatePropertySerializer.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.util.BlockState; + +public class BlockStatePropertySerializer implements PropertySerializer { + @Override + public String serialize(BlockState property) { + return String.valueOf(property.getGlobalId()); + } + + @Override + public BlockState deserialize(String property) { + try { + int id = Integer.parseInt(property); + return new BlockState(id); + } catch (Exception e) { + e.printStackTrace(); + } + return new BlockState(0); + } + + @Override + public Class getTypeClass() { + return BlockState.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/BooleanPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/BooleanPropertySerializer.java new file mode 100644 index 0000000..149bcdb --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/BooleanPropertySerializer.java @@ -0,0 +1,20 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; + +public class BooleanPropertySerializer implements PropertySerializer { + @Override + public String serialize(Boolean property) { + return String.valueOf(property); + } + + @Override + public Boolean deserialize(String property) { + return Boolean.valueOf(property); + } + + @Override + public Class getTypeClass() { + return Boolean.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ColorPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ColorPropertySerializer.java new file mode 100644 index 0000000..e0946a6 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ColorPropertySerializer.java @@ -0,0 +1,21 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; +import org.bukkit.Color; + +public class ColorPropertySerializer implements PropertySerializer { + @Override + public String serialize(Color property) { + return String.valueOf(property.asRGB()); + } + + @Override + public Color deserialize(String property) { + return Color.fromRGB(Integer.parseInt(property)); + } + + @Override + public Class getTypeClass() { + return Color.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ComponentPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ComponentPropertySerializer.java new file mode 100644 index 0000000..c7e3f75 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ComponentPropertySerializer.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class ComponentPropertySerializer implements PropertySerializer { + @Override + public String serialize(Component property) { + return Base64.getEncoder().encodeToString(MiniMessage.miniMessage().serialize(property).getBytes(StandardCharsets.UTF_8)); + } + + @Override + public Component deserialize(String property) { + return MiniMessage.miniMessage().deserialize(new String(Base64.getDecoder().decode(property), StandardCharsets.UTF_8)); + } + + @Override + public Class getTypeClass() { + return Component.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/GenericSerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/GenericSerializer.java new file mode 100644 index 0000000..3ad3a5d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/GenericSerializer.java @@ -0,0 +1,32 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; + +import java.util.function.Function; + +public class GenericSerializer implements PropertySerializer { + private final Function encoder; + private final Function decoder; + private final Class typeClass; + + public GenericSerializer(Function encoder, Function decoder, Class typeClass) { + this.encoder = encoder; + this.decoder = decoder; + this.typeClass = typeClass; + } + + @Override + public String serialize(T property) { + return encoder.apply(property); + } + + @Override + public T deserialize(String property) { + return decoder.apply(property); + } + + @Override + public Class getTypeClass() { + return typeClass; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ItemStackPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ItemStackPropertySerializer.java new file mode 100644 index 0000000..c6f1cb5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ItemStackPropertySerializer.java @@ -0,0 +1,23 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.util.ItemSerializationUtil; + +public class ItemStackPropertySerializer implements PropertySerializer { + @Override + public String serialize(ItemStack property) { + return ItemSerializationUtil.itemToB64(SpigotConversionUtil.toBukkitItemStack(property)); + } + + @Override + public ItemStack deserialize(String property) { + return SpigotConversionUtil.fromBukkitItemStack(ItemSerializationUtil.itemFromB64(property)); + } + + @Override + public Class getTypeClass() { + return ItemStack.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/LookTypeSerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/LookTypeSerializer.java new file mode 100644 index 0000000..0d06d9a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/LookTypeSerializer.java @@ -0,0 +1,26 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.util.LookType; + +public class LookTypeSerializer implements PropertySerializer { + @Override + public String serialize(LookType property) { + return property.name(); + } + + @Override + public LookType deserialize(String property) { + if (property.equals("true")) return LookType.CLOSEST_PLAYER; + try { + return LookType.valueOf(property); + } catch (IllegalArgumentException ignored) { + return LookType.FIXED; + } + } + + @Override + public Class getTypeClass() { + return LookType.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/NamedColorPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/NamedColorPropertySerializer.java new file mode 100644 index 0000000..a227ae7 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/NamedColorPropertySerializer.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.util.NamedColor; + +public class NamedColorPropertySerializer implements PropertySerializer { + @Override + public String serialize(NamedColor property) { + return property.name(); + } + + @Override + public NamedColor deserialize(String property) { + try { + return NamedColor.valueOf(property.toUpperCase()); + } catch (IllegalArgumentException exception) { + return NamedColor.WHITE; + } + } + + @Override + public Class getTypeClass() { + return NamedColor.class; + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/SkinDescriptorSerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/SkinDescriptorSerializer.java new file mode 100644 index 0000000..df59038 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/SkinDescriptorSerializer.java @@ -0,0 +1,29 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.skin.BaseSkinDescriptor; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; + +public class SkinDescriptorSerializer implements PropertySerializer { + private final MojangSkinCache skinCache; + + public SkinDescriptorSerializer(MojangSkinCache skinCache) { + this.skinCache = skinCache; + } + + @Override + public String serialize(SkinDescriptor property) { + return ((BaseSkinDescriptor) property).serialize(); + } + + @Override + public SkinDescriptor deserialize(String property) { + return BaseSkinDescriptor.deserialize(skinCache, property); + } + + @Override + public Class getTypeClass() { + return SkinDescriptor.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/TargetNpcPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/TargetNpcPropertySerializer.java new file mode 100644 index 0000000..7cd3654 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/TargetNpcPropertySerializer.java @@ -0,0 +1,21 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; + +public class TargetNpcPropertySerializer implements PropertySerializer { + @Override + public String serialize(NpcEntryImpl property) { + return property.getId(); + } + + @Override + public NpcEntryImpl deserialize(String property) { + return null; // TODO: find a way to do this + } + + @Override + public Class getTypeClass() { + return NpcEntryImpl.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/Vector3fPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/Vector3fPropertySerializer.java new file mode 100644 index 0000000..efecbd2 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/Vector3fPropertySerializer.java @@ -0,0 +1,22 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.util.Vector3f; + +public class Vector3fPropertySerializer implements PropertySerializer { + + @Override + public String serialize(Vector3f property) { + return property.toString(); + } + + @Override + public Vector3f deserialize(String property) { + return new Vector3f(property); + } + + @Override + public Class getTypeClass() { + return Vector3f.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramImpl.java new file mode 100644 index 0000000..c0194c7 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramImpl.java @@ -0,0 +1,197 @@ +package lol.pyr.znpcsplus.hologram; + +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.znpcsplus.api.hologram.Hologram; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.util.FutureUtil; +import lol.pyr.znpcsplus.util.NpcLocation; +import lol.pyr.znpcsplus.util.Viewable; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class HologramImpl extends Viewable implements Hologram { + private final ConfigManager configManager; + private final PacketFactory packetFactory; + private final LegacyComponentSerializer textSerializer; + private final EntityPropertyRegistryImpl propertyRegistry; + + private double offset = 0.0; + private long refreshDelay = -1; + private long lastRefresh = System.currentTimeMillis(); + private NpcLocation location; + private final List> lines = new ArrayList<>(); + + public HologramImpl(EntityPropertyRegistryImpl propertyRegistry, ConfigManager configManager, PacketFactory packetFactory, LegacyComponentSerializer textSerializer, NpcLocation location) { + this.propertyRegistry = propertyRegistry; + this.configManager = configManager; + this.packetFactory = packetFactory; + this.textSerializer = textSerializer; + this.location = location; + } + + public void addTextLineComponent(Component line) { + HologramText newLine = new HologramText(this, propertyRegistry, packetFactory, null, line); + lines.add(newLine); + relocateLines(); + for (Player viewer : getViewers()) newLine.show(viewer.getPlayer()); + } + + public void addTextLine(String line) { + Component component = line.contains("§") ? Component.text(line) : MiniMessage.miniMessage().deserialize(line); + addTextLineComponent(textSerializer.deserialize(textSerializer.serialize(component))); + } + + public void addItemLineStack(org.bukkit.inventory.ItemStack item) { + addItemLinePEStack(SpigotConversionUtil.fromBukkitItemStack(item)); + } + + public void addItemLine(String serializedItem) { + addItemLinePEStack(HologramItem.deserialize(serializedItem)); + } + + public void addItemLinePEStack(ItemStack item) { + HologramItem newLine = new HologramItem(this, propertyRegistry, packetFactory, null, item); + lines.add(newLine); + relocateLines(); + for (Player viewer : getViewers()) newLine.show(viewer.getPlayer()); + } + + public void addLine(String line) { + if (line.toLowerCase().startsWith("item:")) { + addItemLine(line.substring(5)); + } else { + addTextLine(line); + } + } + + public Component getLineTextComponent(int index) { + return ((HologramText) lines.get(index)).getValue(); + } + + public String getLine(int index) { + if (lines.get(index) instanceof HologramItem) { + return ((HologramItem) lines.get(index)).serialize(); + } else { + return textSerializer.serialize(getLineTextComponent(index)); + } + } + + public void removeLine(int index) { + HologramLine line = lines.remove(index); + for (Player viewer : getViewers()) line.hide(viewer); + relocateLines(); + } + + public List> getLines() { + return Collections.unmodifiableList(lines); + } + + public void clearLines() { + UNSAFE_hideAll(); + lines.clear(); + } + + public void insertTextLineComponent(int index, Component line) { + HologramText newLine = new HologramText(this, propertyRegistry, packetFactory, null, line); + lines.add(index, newLine); + relocateLines(); + for (Player viewer : getViewers()) newLine.show(viewer.getPlayer()); + } + + public void insertTextLine(int index, String line) { + insertTextLineComponent(index, textSerializer.deserialize(textSerializer.serialize(MiniMessage.miniMessage().deserialize(line)))); + } + + public void insertItemLineStack(int index, org.bukkit.inventory.ItemStack item) { + insertItemLinePEStack(index, SpigotConversionUtil.fromBukkitItemStack(item)); + } + + public void insertItemLinePEStack(int index, ItemStack item) { + HologramItem newLine = new HologramItem(this, propertyRegistry, packetFactory, null, item); + lines.add(index, newLine); + relocateLines(); + for (Player viewer : getViewers()) newLine.show(viewer.getPlayer()); + } + + public void insertItemLine(int index, String item) { + insertItemLinePEStack(index, HologramItem.deserialize(item)); + } + + public void insertLine(int index, String line) { + if (line.toLowerCase().startsWith("item:")) { + insertItemLine(index, line.substring(5)); + } else { + insertTextLine(index, line); + } + } + + @Override + public int lineCount() { + return lines.size(); + } + + @Override + protected CompletableFuture UNSAFE_show(Player player) { + return FutureUtil.allOf(lines.stream() + .map(line -> line.show(player)) + .collect(Collectors.toList())); + } + + @Override + protected void UNSAFE_hide(Player player) { + for (HologramLine line : lines) line.hide(player); + } + + @Override + public long getRefreshDelay() { + return refreshDelay; + } + + @Override + public void setRefreshDelay(long refreshDelay) { + this.refreshDelay = refreshDelay; + } + + public boolean shouldRefresh() { + return refreshDelay != -1 && (System.currentTimeMillis() - lastRefresh) > refreshDelay; + } + + public void refresh() { + lastRefresh = System.currentTimeMillis(); + for (HologramLine line : lines) for (Player viewer : getViewers()) line.refreshMeta(viewer); + } + + public void setLocation(NpcLocation location) { + this.location = location; + relocateLines(); + } + + private void relocateLines() { + final double lineSpacing = configManager.getConfig().lineSpacing(); + double height = location.getY() + (lines.size() - 1) * lineSpacing + getOffset(); + for (HologramLine line : lines) { + line.setLocation(location.withY(height)); + height -= lineSpacing; + } + } + + public void setOffset(double offset) { + this.offset = offset; + relocateLines(); + } + + public double getOffset() { + return offset; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramItem.java b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramItem.java new file mode 100644 index 0000000..da17da4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramItem.java @@ -0,0 +1,109 @@ +package lol.pyr.znpcsplus.hologram; + +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import com.github.retrooper.packetevents.protocol.item.type.ItemType; +import com.github.retrooper.packetevents.protocol.item.type.ItemTypes; +import com.github.retrooper.packetevents.protocol.nbt.NBTCompound; +import com.github.retrooper.packetevents.protocol.nbt.NBTInt; +import com.github.retrooper.packetevents.protocol.nbt.NBTNumber; +import com.github.retrooper.packetevents.protocol.nbt.codec.NBTCodec; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.util.NpcLocation; +import lol.pyr.znpcsplus.util.Viewable; + +public class HologramItem extends HologramLine { + public HologramItem(Viewable viewable, EntityPropertyRegistryImpl propertyRegistry, PacketFactory packetFactory, NpcLocation location, ItemStack item) { + super(viewable, item, packetFactory, EntityTypes.ITEM, location); + addProperty(propertyRegistry.getByName("holo_item")); + } + + @SuppressWarnings("unchecked") + @Override + public T getProperty(EntityProperty key) { + if (key.getName().equalsIgnoreCase("holo_item")) return (T) getValue(); + return super.getProperty(key); + } + + @Override + public void setLocation(NpcLocation location) { + super.setLocation(location.withY(location.getY() + 2.05)); + } + + public static boolean ensureValidItemInput(String in) { + if (in == null || in.isEmpty()) { + return false; + } + + int indexOfNbt = in.indexOf("{"); + if (indexOfNbt != -1) { + String typeName = in.substring(0, indexOfNbt); + ItemType type = ItemTypes.getByName("minecraft:" + typeName.toLowerCase()); + if (type == null) { + return false; + } + String nbtString = in.substring(indexOfNbt); + return ensureValidNbt(nbtString); + } else { + ItemType type = ItemTypes.getByName("minecraft:" + in.toLowerCase()); + return type != null; + } + } + + private static boolean ensureValidNbt(String nbtString) { + JsonElement nbtJson; + try { + nbtJson = JsonParser.parseString(nbtString); + } catch (JsonSyntaxException e) { + return false; + } + try { + NBTCodec.jsonToNBT(nbtJson); + } catch (Exception ignored) { + return false; + } + return true; + } + + public static ItemStack deserialize(String serializedItem) { + int indexOfNbt = serializedItem.indexOf("{"); + String typeName = serializedItem; + int amount = 1; + NBTCompound nbt = new NBTCompound(); + if (indexOfNbt != -1) { + typeName = serializedItem.substring(0, indexOfNbt); + String nbtString = serializedItem.substring(indexOfNbt); + JsonElement nbtJson = null; + try { + nbtJson = JsonParser.parseString(nbtString); + } catch (Exception ignored) { + } + if (nbtJson != null) { + nbt = (NBTCompound) NBTCodec.jsonToNBT(nbtJson); + NBTNumber nbtAmount = nbt.getNumberTagOrNull("Count"); + if (nbtAmount != null) { + nbt.removeTag("Count"); + amount = nbtAmount.getAsInt(); + if (amount <= 0) amount = 1; + if (amount > 127) amount = 127; + } + } + } + ItemType type = ItemTypes.getByName("minecraft:" + typeName.toLowerCase()); + if (type == null) type = ItemTypes.STONE; + return ItemStack.builder().type(type).amount(amount).nbt(nbt).build(); + } + + public String serialize() { + NBTCompound nbt = getValue().getNBT(); + if (nbt == null) nbt = new NBTCompound(); + if (getValue().getAmount() > 1) nbt.setTag("Count", new NBTInt(getValue().getAmount())); + if (nbt.isEmpty()) return "item:" + getValue().getType().getName().toString().replace("minecraft:", ""); + return "item:" + getValue().getType().getName().toString().replace("minecraft:", "") + NBTCodec.nbtToJson(nbt, true); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramLine.java b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramLine.java new file mode 100644 index 0000000..553a5e2 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramLine.java @@ -0,0 +1,91 @@ +package lol.pyr.znpcsplus.hologram; + +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.util.NpcLocation; +import lol.pyr.znpcsplus.util.Viewable; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public class HologramLine implements PropertyHolder { + private M value; + private final PacketEntity entity; + private final Set> properties; + + public HologramLine(Viewable viewable, M value, PacketFactory packetFactory, EntityType type, NpcLocation location) { + this.value = value; + this.entity = new PacketEntity(packetFactory, this, viewable, type, location); + this.properties = new HashSet<>(); + } + + public M getValue() { + return value; + } + + public void setValue(M value) { + this.value = value; + } + + public void refreshMeta(Player player) { + entity.refreshMeta(player); + } + + protected CompletableFuture show(Player player) { + return entity.spawn(player); + } + + protected void hide(Player player) { + entity.despawn(player); + } + + public void setLocation(NpcLocation location) { + entity.setLocation(location); + } + + public int getEntityId() { + return entity.getEntityId(); + } + + public void addProperty(EntityProperty property) { + properties.add(property); + } + + @Override + public T getProperty(EntityProperty key) { + return key.getDefaultValue(); + } + + @Override + public boolean hasProperty(EntityProperty key) { + return properties.contains(key); + } + + @Override + public void setProperty(EntityProperty key, T value) { + throw new UnsupportedOperationException("Can't set properties on a hologram line"); + } + + @Override + public void setItemProperty(EntityProperty key, ItemStack value) { + throw new UnsupportedOperationException("Can't set properties on a hologram line"); + } + + @SuppressWarnings("unchecked") + @Override + public ItemStack getItemProperty(EntityProperty key) { + return SpigotConversionUtil.toBukkitItemStack(((EntityProperty) key).getDefaultValue()); + } + + @Override + public Set> getAppliedProperties() { + return properties; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramText.java b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramText.java new file mode 100644 index 0000000..23f3428 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramText.java @@ -0,0 +1,42 @@ +package lol.pyr.znpcsplus.hologram; + +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.util.NpcLocation; +import lol.pyr.znpcsplus.util.Viewable; +import net.kyori.adventure.text.Component; +import org.bukkit.entity.Player; + +import java.util.concurrent.CompletableFuture; + +public class HologramText extends HologramLine { + + private static final Component BLANK = Component.text("%blank%"); + + public HologramText(Viewable viewable, EntityPropertyRegistryImpl propertyRegistry, PacketFactory packetFactory, NpcLocation location, Component text) { + super(viewable, text, packetFactory, EntityTypes.ARMOR_STAND, location); + addProperty(propertyRegistry.getByName("name")); + addProperty(propertyRegistry.getByName("invisible")); + } + + @Override + public CompletableFuture show(Player player) { + if (getValue().equals(BLANK)) return CompletableFuture.completedFuture(null); + return super.show(player); + } + + @SuppressWarnings("unchecked") + @Override + public T getProperty(EntityProperty key) { + if (key.getName().equalsIgnoreCase("invisible")) return (T) Boolean.TRUE; + if (key.getName().equalsIgnoreCase("name")) return (T) getValue(); + return super.getProperty(key); + } + + @Override + public boolean hasProperty(EntityProperty key) { + return key.getName().equalsIgnoreCase("name") || key.getName().equalsIgnoreCase("invisible"); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/ActionFactoryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/ActionFactoryImpl.java new file mode 100644 index 0000000..1e58967 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/ActionFactoryImpl.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.interaction; + +import lol.pyr.znpcsplus.api.interaction.ActionFactory; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.consolecommand.ConsoleCommandAction; +import lol.pyr.znpcsplus.interaction.message.MessageAction; +import lol.pyr.znpcsplus.interaction.playerchat.PlayerChatAction; +import lol.pyr.znpcsplus.interaction.playercommand.PlayerCommandAction; +import lol.pyr.znpcsplus.interaction.switchserver.SwitchServerAction; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.util.BungeeConnector; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +public class ActionFactoryImpl implements ActionFactory { + private final TaskScheduler scheduler; + private final BukkitAudiences adventure; + private final LegacyComponentSerializer textSerializer; + private final BungeeConnector bungeeConnector; + + public ActionFactoryImpl(TaskScheduler scheduler, BukkitAudiences adventure, LegacyComponentSerializer textSerializer, BungeeConnector bungeeConnector) { + this.scheduler = scheduler; + this.adventure = adventure; + this.textSerializer = textSerializer; + this.bungeeConnector = bungeeConnector; + } + + public InteractionAction createConsoleCommandAction(String command, InteractionType interactionType, long cooldown, long delay) { + return new ConsoleCommandAction(this.scheduler, command, interactionType, cooldown, delay); + } + + public InteractionAction createMessageAction(String message, InteractionType interactionType, long cooldown, long delay) { + return new MessageAction(this.adventure, textSerializer, message, interactionType, cooldown, delay); + } + + public InteractionAction createPlayerChatAction(String message, InteractionType interactionType, long cooldown, long delay) { + return new PlayerChatAction(this.scheduler, message, interactionType, cooldown, delay); + } + + public InteractionAction createPlayerCommandAction(String command, InteractionType interactionType, long cooldown, long delay) { + return new PlayerCommandAction(this.scheduler, command, interactionType, cooldown, delay); + } + + public InteractionAction createSwitchServerAction(String server, InteractionType interactionType, long cooldown, long delay) { + return new SwitchServerAction(bungeeConnector, server, interactionType, cooldown, delay); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/ActionRegistryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/ActionRegistryImpl.java new file mode 100644 index 0000000..b08ae1c --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/ActionRegistryImpl.java @@ -0,0 +1,65 @@ +package lol.pyr.znpcsplus.interaction; + +import lol.pyr.znpcsplus.api.interaction.*; +import lol.pyr.znpcsplus.interaction.consolecommand.ConsoleCommandActionType; +import lol.pyr.znpcsplus.interaction.message.MessageActionType; +import lol.pyr.znpcsplus.interaction.playerchat.PlayerChatActionType; +import lol.pyr.znpcsplus.interaction.playercommand.PlayerCommandActionType; +import lol.pyr.znpcsplus.interaction.switchserver.SwitchServerActionType; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.util.BungeeConnector; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class ActionRegistryImpl implements ActionRegistry { + private final Map, InteractionActionType> serializerMap = new HashMap<>(); + + public void registerTypes(TaskScheduler taskScheduler, BukkitAudiences adventure, LegacyComponentSerializer textSerializer, BungeeConnector bungeeConnector) { + register(new ConsoleCommandActionType(taskScheduler)); + register(new PlayerCommandActionType(taskScheduler)); + register(new SwitchServerActionType(bungeeConnector)); + register(new MessageActionType(adventure, textSerializer)); + register(new PlayerChatActionType(taskScheduler)); + } + + public void register(InteractionActionType type) { + serializerMap.put(type.getActionClass(), type); + } + + public void unregister(Class clazz) { + serializerMap.remove(clazz); + } + + public List getCommands() { + return serializerMap.values().stream() + .filter(type -> type instanceof InteractionCommandHandler) + .map(type -> (InteractionCommandHandler) type) + .collect(Collectors.toList()); + } + + @SuppressWarnings("unchecked") + public T deserialize(String str) { + try { + String[] split = str.split(";"); + Class clazz = Class.forName(split[0]); + InteractionActionType serializer = (InteractionActionType) serializerMap.get(clazz); + if (serializer == null) return null; + return serializer.deserialize(String.join(";", Arrays.copyOfRange(split, 1, split.length))); + } catch (ClassNotFoundException e) { + return null; + } + } + + @SuppressWarnings("unchecked") + public String serialize(T action) { + InteractionActionType serializer = (InteractionActionType) serializerMap.get(action.getClass()); + if (serializer == null) return null; + return action.getClass().getName() + ";" + serializer.serialize(action); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionActionImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionActionImpl.java new file mode 100644 index 0000000..ed7539e --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionActionImpl.java @@ -0,0 +1,14 @@ +package lol.pyr.znpcsplus.interaction; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import net.kyori.adventure.text.Component; + +public abstract class InteractionActionImpl extends InteractionAction { + protected InteractionActionImpl(long cooldown, long delay, InteractionType interactionType) { + super(cooldown, delay, interactionType); + } + + public abstract Component getInfo(String id, int index, CommandContext context); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionCommandHandler.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionCommandHandler.java new file mode 100644 index 0000000..93d1fbc --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionCommandHandler.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.interaction; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +public interface InteractionCommandHandler extends CommandHandler { + String getSubcommandName(); + + InteractionAction parse(CommandContext context) throws CommandExecutionException; + void appendUsage(CommandContext context); + + @Override + default void run(CommandContext context) throws CommandExecutionException { + appendUsage(context); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + npc.addAction(parse(context)); + context.send(Component.text("Added action to npc", NamedTextColor.GREEN)); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionPacketListener.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionPacketListener.java new file mode 100644 index 0000000..8734c0e --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionPacketListener.java @@ -0,0 +1,87 @@ +package lol.pyr.znpcsplus.interaction; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.event.PacketListener; +import com.github.retrooper.packetevents.event.PacketReceiveEvent; +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.player.Equipment; +import com.github.retrooper.packetevents.protocol.player.EquipmentSlot; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityEquipment; +import lol.pyr.znpcsplus.api.event.NpcInteractEvent; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.user.User; +import lol.pyr.znpcsplus.user.UserManager; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.Collections; + +public class InteractionPacketListener implements PacketListener { + private final UserManager userManager; + private final NpcRegistryImpl npcRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final TaskScheduler scheduler; + + public InteractionPacketListener(UserManager userManager, NpcRegistryImpl npcRegistry, NpcTypeRegistryImpl typeRegistry, TaskScheduler scheduler) { + this.userManager = userManager; + this.npcRegistry = npcRegistry; + this.typeRegistry = typeRegistry; + this.scheduler = scheduler; + } + + @Override + public void onPacketReceive(PacketReceiveEvent event) { + if (event.getPacketType() != PacketType.Play.Client.INTERACT_ENTITY) return; + Player player = (Player) event.getPlayer(); + if (player == null) return; + + WrapperPlayClientInteractEntity packet = new WrapperPlayClientInteractEntity(event); + + NpcEntryImpl entry = npcRegistry.getByEntityId(packet.getEntityId()); + if (entry == null || !entry.isProcessed()) return; + NpcImpl npc = entry.getNpc(); + + if ((packet.getAction().equals(WrapperPlayClientInteractEntity.InteractAction.INTERACT) + || packet.getAction().equals(WrapperPlayClientInteractEntity.InteractAction.INTERACT_AT)) + && npc.getType().equals(typeRegistry.getByName("allay"))) { + PacketEvents.getAPI().getPlayerManager().sendPacket(player, + new WrapperPlayServerEntityEquipment(packet.getEntityId(), Collections.singletonList( + new Equipment(EquipmentSlot.MAIN_HAND, ItemStack.EMPTY)))); + player.updateInventory(); + } + + InteractionType type = wrapClickType(packet.getAction()); + + User user = userManager.get(player); + if (!user.canInteract()) return; + + NpcInteractEvent interactEvent = new NpcInteractEvent(player, entry, type); + Bukkit.getPluginManager().callEvent(interactEvent); + if (interactEvent.isCancelled()) return; + + for (InteractionAction action : npc.getActions()) { + if (action.getInteractionType() != InteractionType.ANY_CLICK && action.getInteractionType() != type) continue; + if (action.getCooldown() > 0 && !user.actionCooldownCheck(action)) continue; + scheduler.runLaterAsync(() -> action.run(player), action.getDelay()); + } + } + + private InteractionType wrapClickType(WrapperPlayClientInteractEntity.InteractAction action) { + switch (action) { + case ATTACK: + return InteractionType.LEFT_CLICK; + case INTERACT: + case INTERACT_AT: + return InteractionType.RIGHT_CLICK; + } + throw new IllegalStateException(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/consolecommand/ConsoleCommandAction.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/consolecommand/ConsoleCommandAction.java new file mode 100644 index 0000000..1b0b040 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/consolecommand/ConsoleCommandAction.java @@ -0,0 +1,55 @@ +package lol.pyr.znpcsplus.interaction.consolecommand; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.util.PapiUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public class ConsoleCommandAction extends InteractionActionImpl { + private final TaskScheduler scheduler; + private final String command; + + public ConsoleCommandAction(TaskScheduler scheduler, String command, InteractionType interactionType, long cooldown, long delay) { + super(cooldown, delay, interactionType); + this.scheduler = scheduler; + this.command = command; + } + + @Override + public void run(Player player) { + String cmd = command.replace("{player}", player.getName()).replace("{uuid}", player.getUniqueId().toString()); + scheduler.runSyncGlobal(() -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), PapiUtil.set(player, cmd))); + } + + @Override + public Component getInfo(String id, int index, CommandContext context) { + return Component.text(index + ") ", NamedTextColor.GOLD) + .append(Component.text("[EDIT]", NamedTextColor.DARK_GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to edit this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action edit " + id + " " + index + " consolecommand " + getInteractionType().name() + " " + getCooldown()/1000 + " " + getDelay() + " " + command)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("[DELETE]", NamedTextColor.RED) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to delete this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action delete " + id + " " + index))) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Console Command: ", NamedTextColor.GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click Type: " + getInteractionType().name() + " Cooldown: " + getCooldown()/1000 + " Delay: " + getDelay(), NamedTextColor.GRAY)))) + .append(Component.text(command, NamedTextColor.WHITE))); + } + + public String getCommand() { + return command; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/consolecommand/ConsoleCommandActionType.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/consolecommand/ConsoleCommandActionType.java new file mode 100644 index 0000000..931256c --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/consolecommand/ConsoleCommandActionType.java @@ -0,0 +1,66 @@ +package lol.pyr.znpcsplus.interaction.consolecommand; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.api.interaction.InteractionActionType; +import lol.pyr.znpcsplus.interaction.InteractionCommandHandler; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +public class ConsoleCommandActionType implements InteractionActionType, InteractionCommandHandler { + private final TaskScheduler scheduler; + + public ConsoleCommandActionType(TaskScheduler scheduler) { + this.scheduler = scheduler; + } + + @Override + public String serialize(ConsoleCommandAction obj) { + return Base64.getEncoder().encodeToString(obj.getCommand().getBytes(StandardCharsets.UTF_8)) + ";" + obj.getCooldown() + ";" + obj.getInteractionType().name() + ";" + obj.getDelay(); + } + + @Override + public ConsoleCommandAction deserialize(String str) { + String[] split = str.split(";"); + InteractionType type = split.length > 2 ? InteractionType.valueOf(split[2]) : InteractionType.ANY_CLICK; + return new ConsoleCommandAction(scheduler, new String(Base64.getDecoder().decode(split[0]), StandardCharsets.UTF_8), type, Long.parseLong(split[1]), Long.parseLong(split.length > 3 ? split[3] : "0")); + } + + @Override + public Class getActionClass() { + return ConsoleCommandAction.class; + } + + @Override + public String getSubcommandName() { + return "consolecommand"; + } + + @Override + public void appendUsage(CommandContext context) { + context.setUsage(context.getUsage() + " " + getSubcommandName() + " "); + } + + @Override + public InteractionActionImpl parse(CommandContext context) throws CommandExecutionException { + InteractionType type = context.parse(InteractionType.class); + long cooldown = (long) (context.parse(Double.class) * 1000D); + long delay = (long) (context.parse(Integer.class) * 1D); + String command = context.dumpAllArgs(); + return new ConsoleCommandAction(scheduler, command, type, cooldown, delay); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestEnum(InteractionType.values()); + if (context.argSize() == 2) return context.suggestLiteral("1"); + if (context.argSize() == 3) return context.suggestLiteral("0"); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/message/MessageAction.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/message/MessageAction.java new file mode 100644 index 0000000..051bea5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/message/MessageAction.java @@ -0,0 +1,58 @@ +package lol.pyr.znpcsplus.interaction.message; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.util.PapiUtil; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; + +public class MessageAction extends InteractionActionImpl { + private final BukkitAudiences adventure; + private final String message; + private final LegacyComponentSerializer textSerializer; + + public MessageAction(BukkitAudiences adventure, LegacyComponentSerializer textSerializer, String message, InteractionType interactionType, long cooldown, long delay) { + super(cooldown, delay, interactionType); + this.adventure = adventure; + this.message = message; + this.textSerializer = textSerializer; + } + + @Override + public void run(Player player) { + String msg = message.replace("{player}", player.getName()) + .replace("{uuid}", player.getUniqueId().toString()); + adventure.player(player).sendMessage(textSerializer.deserialize(PapiUtil.set(player, msg))); + } + + @Override + public Component getInfo(String id, int index, CommandContext context) { + return Component.text(index + ") ", NamedTextColor.GOLD) + .append(Component.text("[EDIT]", NamedTextColor.DARK_GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to edit this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action edit " + id + " " + index + " message " + getInteractionType().name() + " " + getCooldown()/1000 + " " + getDelay() + " " + message)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("[DELETE]", NamedTextColor.RED) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to delete this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action delete " + id + " " + index))) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Message: ", NamedTextColor.GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click Type: " + getInteractionType().name() + " Cooldown: " + getCooldown()/1000 + " Delay: " + getDelay(), NamedTextColor.GRAY)))) + .append(Component.text(message, NamedTextColor.WHITE))); + } + + public String getMessage() { + return message; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/message/MessageActionType.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/message/MessageActionType.java new file mode 100644 index 0000000..54dbfae --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/message/MessageActionType.java @@ -0,0 +1,69 @@ +package lol.pyr.znpcsplus.interaction.message; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.api.interaction.InteractionActionType; +import lol.pyr.znpcsplus.interaction.InteractionCommandHandler; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +public class MessageActionType implements InteractionActionType, InteractionCommandHandler { + private final BukkitAudiences adventure; + private final LegacyComponentSerializer textSerializer; + + public MessageActionType(BukkitAudiences adventure, LegacyComponentSerializer textSerializer) { + this.adventure = adventure; + this.textSerializer = textSerializer; + } + + @Override + public String serialize(MessageAction obj) { + return Base64.getEncoder().encodeToString(obj.getMessage().getBytes(StandardCharsets.UTF_8)) + ";" + obj.getCooldown() + ";" + obj.getInteractionType().name() + ";" + obj.getDelay(); + } + + @Override + public MessageAction deserialize(String str) { + String[] split = str.split(";"); + InteractionType type = split.length > 2 ? InteractionType.valueOf(split[2]) : InteractionType.ANY_CLICK; + return new MessageAction(adventure, textSerializer, new String(Base64.getDecoder().decode(split[0]), StandardCharsets.UTF_8), type, Long.parseLong(split[1]), Long.parseLong(split.length > 3 ? split[3] : "0")); + } + + @Override + public Class getActionClass() { + return MessageAction.class; + } + + @Override + public String getSubcommandName() { + return "message"; + } + + @Override + public void appendUsage(CommandContext context) { + context.setUsage(context.getUsage() + " " + getSubcommandName() + " "); + } + + @Override + public InteractionActionImpl parse(CommandContext context) throws CommandExecutionException { + InteractionType type = context.parse(InteractionType.class); + long cooldown = (long) (context.parse(Double.class) * 1000D); + long delay = (long) (context.parse(Integer.class) * 1D); + String message = context.dumpAllArgs(); + return new MessageAction(adventure, textSerializer, message, type, cooldown, delay); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestEnum(InteractionType.values()); + if (context.argSize() == 2) return context.suggestLiteral("1"); + if (context.argSize() == 3) return context.suggestLiteral("0"); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playerchat/PlayerChatAction.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playerchat/PlayerChatAction.java new file mode 100644 index 0000000..ab3ffe0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playerchat/PlayerChatAction.java @@ -0,0 +1,53 @@ +package lol.pyr.znpcsplus.interaction.playerchat; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public class PlayerChatAction extends InteractionActionImpl { + private final String message; + private final TaskScheduler scheduler; + + public PlayerChatAction(TaskScheduler scheduler, String message, InteractionType interactionType, long cooldown, long delay) { + super(cooldown, delay, interactionType); + this.message = message; + this.scheduler = scheduler; + } + + @Override + public void run(Player player) { + scheduler.schedulePlayerChat(player, message.replace("{player}", player.getName()) + .replace("{uuid}", player.getUniqueId().toString())); + } + + @Override + public Component getInfo(String id, int index, CommandContext context) { + return Component.text(index + ") ", NamedTextColor.GOLD) + .append(Component.text("[EDIT]", NamedTextColor.DARK_GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to edit this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action edit " + id + " " + index + " playerchat " + getInteractionType().name() + " " + getCooldown()/1000 + " " + getDelay() + " " + message)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("[DELETE]", NamedTextColor.RED) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to delete this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action delete " + id + " " + index))) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Player Chat: ", NamedTextColor.GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click Type: " + getInteractionType().name() + " Cooldown: " + getCooldown()/1000 + " Delay: " + getDelay(), NamedTextColor.GRAY)))) + .append(Component.text(message, NamedTextColor.WHITE))); + } + + public String getMessage() { + return message; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playerchat/PlayerChatActionType.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playerchat/PlayerChatActionType.java new file mode 100644 index 0000000..99a292d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playerchat/PlayerChatActionType.java @@ -0,0 +1,65 @@ +package lol.pyr.znpcsplus.interaction.playerchat; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.interaction.InteractionActionType; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionCommandHandler; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +public class PlayerChatActionType implements InteractionActionType, InteractionCommandHandler { + private final TaskScheduler scheduler; + + public PlayerChatActionType(TaskScheduler scheduler) { + this.scheduler = scheduler; + } + + @Override + public String serialize(PlayerChatAction obj) { + return Base64.getEncoder().encodeToString(obj.getMessage().getBytes(StandardCharsets.UTF_8)) + ";" + obj.getCooldown() + ";" + obj.getInteractionType().name() + ";" + obj.getDelay(); + } + + @Override + public PlayerChatAction deserialize(String str) { + String[] split = str.split(";"); + return new PlayerChatAction(scheduler, new String(Base64.getDecoder().decode(split[0]), StandardCharsets.UTF_8), InteractionType.valueOf(split[2]), Long.parseLong(split[1]), Long.parseLong(split.length > 3 ? split[3] : "0")); + } + + @Override + public Class getActionClass() { + return PlayerChatAction.class; + } + + @Override + public String getSubcommandName() { + return "playerchat"; + } + + @Override + public void appendUsage(CommandContext context) { + context.setUsage(context.getUsage() + " " + getSubcommandName() + " "); + } + + @Override + public InteractionAction parse(CommandContext context) throws CommandExecutionException { + InteractionType type = context.parse(InteractionType.class); + long cooldown = (long) (context.parse(Double.class) * 1000D); + long delay = (long) (context.parse(Integer.class) * 1D); + String message = context.dumpAllArgs(); + return new PlayerChatAction(scheduler, message, type, cooldown, delay); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestEnum(InteractionType.values()); + if (context.argSize() == 2) return context.suggestLiteral("1"); + if (context.argSize() == 3) return context.suggestLiteral("0"); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playercommand/PlayerCommandAction.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playercommand/PlayerCommandAction.java new file mode 100644 index 0000000..65ab64f --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playercommand/PlayerCommandAction.java @@ -0,0 +1,54 @@ +package lol.pyr.znpcsplus.interaction.playercommand; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.util.PapiUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public class PlayerCommandAction extends InteractionActionImpl { + private final TaskScheduler scheduler; + private final String command; + + public PlayerCommandAction(TaskScheduler scheduler, String command, InteractionType interactionType, long cooldown, long delay) { + super(cooldown, delay, interactionType); + this.scheduler = scheduler; + this.command = command; + } + + @Override + public void run(Player player) { + String cmd = command.replace("{player}", player.getName()).replace("{uuid}", player.getUniqueId().toString()); + scheduler.schedulePlayerCommand(player, PapiUtil.set(player, cmd)); + } + + @Override + public Component getInfo(String id, int index, CommandContext context) { + return Component.text(index + ") ", NamedTextColor.GOLD) + .append(Component.text("[EDIT]", NamedTextColor.DARK_GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to edit this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action edit " + id + " " + index + " playercommand " + getInteractionType().name() + " " + getCooldown()/1000 + " " + getDelay() + " " + command)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("[DELETE]", NamedTextColor.RED) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to delete this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action delete " + id + " " + index))) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Player Command: ", NamedTextColor.GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click Type: " + getInteractionType().name() + " Cooldown: " + getCooldown()/1000 + " Delay: " + getDelay(), NamedTextColor.GRAY)))) + .append(Component.text(command, NamedTextColor.WHITE))); + } + + public String getCommand() { + return command; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playercommand/PlayerCommandActionType.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playercommand/PlayerCommandActionType.java new file mode 100644 index 0000000..d640683 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playercommand/PlayerCommandActionType.java @@ -0,0 +1,66 @@ +package lol.pyr.znpcsplus.interaction.playercommand; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.api.interaction.InteractionActionType; +import lol.pyr.znpcsplus.interaction.InteractionCommandHandler; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +public class PlayerCommandActionType implements InteractionActionType, InteractionCommandHandler { + private final TaskScheduler scheduler; + + public PlayerCommandActionType(TaskScheduler scheduler) { + this.scheduler = scheduler; + } + + @Override + public String serialize(PlayerCommandAction obj) { + return Base64.getEncoder().encodeToString(obj.getCommand().getBytes(StandardCharsets.UTF_8)) + ";" + obj.getCooldown() + ";" + obj.getInteractionType().name() + ";" + obj.getDelay(); + } + + @Override + public PlayerCommandAction deserialize(String str) { + String[] split = str.split(";"); + InteractionType type = split.length > 2 ? InteractionType.valueOf(split[2]) : InteractionType.ANY_CLICK; + return new PlayerCommandAction(scheduler, new String(Base64.getDecoder().decode(split[0]), StandardCharsets.UTF_8), type, Long.parseLong(split[1]), Long.parseLong(split.length > 3 ? split[3] : "0")); + } + + @Override + public Class getActionClass() { + return PlayerCommandAction.class; + } + + @Override + public String getSubcommandName() { + return "playercommand"; + } + + @Override + public void appendUsage(CommandContext context) { + context.setUsage(context.getUsage() + " " + getSubcommandName() + " "); + } + + @Override + public InteractionActionImpl parse(CommandContext context) throws CommandExecutionException { + InteractionType type = context.parse(InteractionType.class); + long cooldown = (long) (context.parse(Double.class) * 1000D); + long delay = (long) (context.parse(Integer.class) * 1D); + String command = context.dumpAllArgs(); + return new PlayerCommandAction(scheduler, command, type, cooldown, delay); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestEnum(InteractionType.values()); + if (context.argSize() == 2) return context.suggestLiteral("1"); + if (context.argSize() == 3) return context.suggestLiteral("0"); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/switchserver/SwitchServerAction.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/switchserver/SwitchServerAction.java new file mode 100644 index 0000000..ca89839 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/switchserver/SwitchServerAction.java @@ -0,0 +1,52 @@ +package lol.pyr.znpcsplus.interaction.switchserver; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.util.BungeeConnector; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public class SwitchServerAction extends InteractionActionImpl { + private final String server; + private final BungeeConnector bungeeConnector; + + public SwitchServerAction(BungeeConnector bungeeConnector, String server, InteractionType interactionType, long cooldown, long delay) { + super(cooldown, delay, interactionType); + this.server = server; + this.bungeeConnector = bungeeConnector; + } + + @Override + public void run(Player player) { + bungeeConnector.connectPlayer(player, server); + } + + @Override + public Component getInfo(String id, int index, CommandContext context) { + return Component.text(index + ") ", NamedTextColor.GOLD) + .append(Component.text("[EDIT]", NamedTextColor.DARK_GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to edit this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action edit " + id + " " + index + " switchserver " + getInteractionType().name() + " " + getCooldown()/1000 + " " + getDelay() + " " + server)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("[DELETE]", NamedTextColor.RED) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to delete this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action delete " + id + " " + index))) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Switch Server: ", NamedTextColor.GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click Type: " + getInteractionType().name() + " Cooldown: " + getCooldown()/1000 + " Delay: " + getDelay(), NamedTextColor.GRAY)))) + .append(Component.text(server, NamedTextColor.WHITE))); + } + + public String getServer() { + return server; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/switchserver/SwitchServerActionType.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/switchserver/SwitchServerActionType.java new file mode 100644 index 0000000..ead4fa3 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/switchserver/SwitchServerActionType.java @@ -0,0 +1,66 @@ +package lol.pyr.znpcsplus.interaction.switchserver; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.api.interaction.InteractionActionType; +import lol.pyr.znpcsplus.interaction.InteractionCommandHandler; +import lol.pyr.znpcsplus.util.BungeeConnector; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +public class SwitchServerActionType implements InteractionActionType, InteractionCommandHandler { + private final BungeeConnector bungeeConnector; + + public SwitchServerActionType(BungeeConnector bungeeConnector) { + this.bungeeConnector = bungeeConnector; + } + + @Override + public String serialize(SwitchServerAction obj) { + return Base64.getEncoder().encodeToString(obj.getServer().getBytes(StandardCharsets.UTF_8)) + ";" + obj.getCooldown() + ";" + obj.getInteractionType().name() + ";" + obj.getDelay(); + } + + @Override + public SwitchServerAction deserialize(String str) { + String[] split = str.split(";"); + InteractionType type = split.length > 2 ? InteractionType.valueOf(split[2]) : InteractionType.ANY_CLICK; + return new SwitchServerAction(bungeeConnector, new String(Base64.getDecoder().decode(split[0]), StandardCharsets.UTF_8), type, Long.parseLong(split[1]), Long.parseLong(split.length > 3 ? split[3] : "0")); + } + + @Override + public Class getActionClass() { + return SwitchServerAction.class; + } + + @Override + public String getSubcommandName() { + return "switchserver"; + } + + @Override + public void appendUsage(CommandContext context) { + context.setUsage(context.getUsage() + " " + getSubcommandName() + " "); + } + + @Override + public InteractionActionImpl parse(CommandContext context) throws CommandExecutionException { + InteractionType type = context.parse(InteractionType.class); + long cooldown = (long) (context.parse(Double.class) * 1000D); + long delay = (long) (context.parse(Integer.class) * 1D); + String server = context.dumpAllArgs(); + return new SwitchServerAction(bungeeConnector, server, type, cooldown, delay); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestEnum(InteractionType.values()); + if (context.argSize() == 2) return context.suggestLiteral("1"); + if (context.argSize() == 3) return context.suggestLiteral("0"); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcEntryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcEntryImpl.java new file mode 100644 index 0000000..e944dc0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcEntryImpl.java @@ -0,0 +1,63 @@ +package lol.pyr.znpcsplus.npc; + +import lol.pyr.znpcsplus.api.npc.NpcEntry; + +public class NpcEntryImpl implements NpcEntry { + private final String id; + private final NpcImpl npc; + + private boolean process = false; + private boolean save = false; + private boolean modify = false; + + public NpcEntryImpl(String id, NpcImpl npc) { + this.id = id.toLowerCase(); + this.npc = npc; + } + + @Override + public NpcImpl getNpc() { + return npc; + } + + @Override + public boolean isProcessed() { + return process; + } + + @Override + public void setProcessed(boolean value) { + if (process && !value) npc.delete(); + process = value; + } + + @Override + public boolean isSave() { + return save; + } + + @Override + public void setSave(boolean value) { + save = value; + } + + @Override + public boolean isAllowCommandModification() { + return modify; + } + + @Override + public void setAllowCommandModification(boolean value) { + modify = value; + } + + public void enableEverything() { + setSave(true); + setProcessed(true); + setAllowCommandModification(true); + } + + public String getId() { + return id; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcImpl.java new file mode 100644 index 0000000..97c82f5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcImpl.java @@ -0,0 +1,296 @@ +package lol.pyr.znpcsplus.npc; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.npc.Npc; +import lol.pyr.znpcsplus.api.npc.NpcType; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.util.NpcLocation; +import lol.pyr.znpcsplus.util.Viewable; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class NpcImpl extends Viewable implements Npc { + private final PacketFactory packetFactory; + private String worldName; + private PacketEntity entity; + private NpcLocation location; + private NpcTypeImpl type; + private boolean enabled = true; + private final HologramImpl hologram; + private final UUID uuid; + + private final Map, Object> propertyMap = new HashMap<>(); + private final List actions = new ArrayList<>(); + + private final Map playerLookMap = new ConcurrentHashMap<>(); + + protected NpcImpl(UUID uuid, EntityPropertyRegistryImpl propertyRegistry, ConfigManager configManager, LegacyComponentSerializer textSerializer, World world, NpcTypeImpl type, NpcLocation location, PacketFactory packetFactory) { + this(uuid, propertyRegistry, configManager, packetFactory, textSerializer, world.getName(), type, location); + } + + public NpcImpl(UUID uuid, EntityPropertyRegistryImpl propertyRegistry, ConfigManager configManager, PacketFactory packetFactory, LegacyComponentSerializer textSerializer, String world, NpcTypeImpl type, NpcLocation location) { + this.packetFactory = packetFactory; + this.worldName = world; + this.type = type; + this.location = location; + this.uuid = uuid; + entity = new PacketEntity(packetFactory, this, this, type.getType(), location); + hologram = new HologramImpl(propertyRegistry, configManager, packetFactory, textSerializer, location.withY(location.getY() + type.getHologramOffset())); + } + + public void setType(NpcTypeImpl type) { + UNSAFE_hideAll(); + this.type = type; + entity = new PacketEntity(packetFactory, this, this, type.getType(), entity.getLocation()); + hologram.setLocation(location.withY(location.getY() + type.getHologramOffset())); + UNSAFE_showAll(); + } + + public void setType(NpcType type) { + if (type == null) throw new IllegalArgumentException("Npc Type cannot be null"); + setType((NpcTypeImpl) type); + } + + public NpcTypeImpl getType() { + return type; + } + + public PacketEntity getEntity() { + return entity; + } + + public NpcLocation getLocation() { + return location; + } + + public @Nullable Location getBukkitLocation() { + World world = getWorld(); + if (world == null) return null; + return location.toBukkitLocation(world); + } + + public void setLocation(NpcLocation location) { + this.location = location; + playerLookMap.clear(); + playerLookMap.putAll(getViewers().stream().collect(Collectors.toMap(Player::getUniqueId, player -> new float[]{location.getYaw(), location.getPitch()}))); + entity.setLocation(location); + hologram.setLocation(location.withY(location.getY() + type.getHologramOffset())); + } + + public void setHeadRotation(Player player, float yaw, float pitch) { + if (getHeadYaw(player) == yaw && getHeadPitch(player) == pitch) return; + playerLookMap.put(player.getUniqueId(), new float[]{yaw, pitch}); + entity.setHeadRotation(player, yaw, pitch); + } + + public void setHeadRotation(float yaw, float pitch) { + for (Player player : getViewers()) { + if (getHeadYaw(player) == yaw && getHeadPitch(player) == pitch) continue; + playerLookMap.put(player.getUniqueId(), new float[]{yaw, pitch}); + entity.setHeadRotation(player, yaw, pitch); + } + } + + public float getHeadYaw(Player player) { + return playerLookMap.getOrDefault(player.getUniqueId(), new float[]{location.getYaw(), location.getPitch()})[0]; + } + + public float getHeadPitch(Player player) { + return playerLookMap.getOrDefault(player.getUniqueId(), new float[]{location.getYaw(), location.getPitch()})[1]; + } + + public HologramImpl getHologram() { + return hologram; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + if (!enabled) delete(); + } + + public boolean isEnabled() { + return enabled; + } + + public UUID getUuid() { + return uuid; + } + + public @Nullable World getWorld() { + return Bukkit.getWorld(worldName); + } + + public String getWorldName() { + return worldName; + } + + @Override + protected CompletableFuture UNSAFE_show(Player player) { + playerLookMap.put(player.getUniqueId(), new float[]{location.getYaw(), location.getPitch()}); + return CompletableFuture.allOf(entity.spawn(player), hologram.show(player)); + } + + @Override + protected void UNSAFE_hide(Player player) { + playerLookMap.remove(player.getUniqueId()); + entity.despawn(player); + hologram.hide(player); + } + + private void UNSAFE_refreshProperty(EntityPropertyImpl property) { + if (!type.isAllowedProperty(property)) return; + for (Player viewer : getViewers()) { + List> data = property.applyStandalone(viewer, entity, true); + if (!data.isEmpty()) packetFactory.sendMetadata(viewer, entity, data); + } + } + + @SuppressWarnings("unchecked") + public T getProperty(EntityProperty key) { + return hasProperty(key) ? (T) propertyMap.get((EntityPropertyImpl) key) : key.getDefaultValue(); + } + + public boolean hasProperty(EntityProperty key) { + return propertyMap.containsKey((EntityPropertyImpl) key); + } + + @SuppressWarnings("unchecked") + @Override + public void setProperty(EntityProperty key, T value) { + // See https://github.com/Pyrbu/ZNPCsPlus/pull/129#issuecomment-1948777764 + Object val = value; + if (val instanceof ItemStack) val = SpigotConversionUtil.fromBukkitItemStack((ItemStack) val); + + setProperty((EntityPropertyImpl) key, (T) val); + } + + @SuppressWarnings("unchecked") + @Override + public void setItemProperty(EntityProperty key, ItemStack value) { + setProperty((EntityPropertyImpl) key, SpigotConversionUtil.fromBukkitItemStack(value)); + } + + @SuppressWarnings("unchecked") + @Override + public ItemStack getItemProperty(EntityProperty key) { + return SpigotConversionUtil.toBukkitItemStack(getProperty((EntityProperty) key)); + } + + public void setProperty(EntityPropertyImpl key, T value) { + if (key == null) return; + if (value == null || value.equals(key.getDefaultValue())) propertyMap.remove(key); + else propertyMap.put(key, value); + UNSAFE_refreshProperty(key); + } + + @SuppressWarnings("unchecked") + public void UNSAFE_setProperty(EntityPropertyImpl property, Object value) { + setProperty((EntityPropertyImpl) property, (T) value); + } + + @SuppressWarnings("unchecked") + public void UNSAFE_setProperty(EntityProperty property, Object value) { + setProperty((EntityPropertyImpl) property, (T) value); + } + + public Set> getAllProperties() { + return Collections.unmodifiableSet(propertyMap.keySet()); + } + + @Override + public Set> getAppliedProperties() { + return Collections.unmodifiableSet(propertyMap.keySet()).stream().filter(type::isAllowedProperty).collect(Collectors.toSet()); + } + + @Override + public List getActions() { + return Collections.unmodifiableList(actions); + } + + @Override + public void removeAction(int index) { + actions.remove(index); + } + + @Override + public void addAction(InteractionAction action) throws IllegalArgumentException { + if (action == null) throw new IllegalArgumentException("action can not be null"); + actions.add(action); + } + + @Override + public void clearActions() { + actions.clear(); + } + + @Override + public void editAction(int index, InteractionAction action) throws IllegalArgumentException { + if (action == null) throw new IllegalArgumentException("action can not be null"); + actions.set(index, action); + } + + @Override + public int getPacketEntityId() { + return entity.getEntityId(); + } + + public void setWorld(World world) { + if (world == null) throw new IllegalArgumentException("world can not be null"); + delete(); + this.worldName = world.getName(); + } + + public void setWorld(String name) { + if (name == null) throw new IllegalArgumentException("world name can not be null"); + delete(); + this.worldName = name; + } + + public void swingHand(boolean offHand) { + for (Player viewer : getViewers()) entity.swingHand(viewer, offHand); + } + + @Override + public @NotNull List getPassengers() { + return entity.getPassengers(); + } + + @Override + public void addPassenger(int entityId) { + entity.addPassenger(entityId); + } + + @Override + public void removePassenger(int entityId) { + entity.removePassenger(entityId); + } + + @Override + public @Nullable Integer getVehicleId() { + return entity.getVehicleId(); + } + + @Override + public void setVehicleId(Integer vehicleId) { + entity.setVehicleId(vehicleId); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcRegistryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcRegistryImpl.java new file mode 100644 index 0000000..43f0a9f --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcRegistryImpl.java @@ -0,0 +1,231 @@ +package lol.pyr.znpcsplus.npc; + +import lol.pyr.znpcsplus.ZNpcsPlus; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.npc.NpcEntry; +import lol.pyr.znpcsplus.api.npc.NpcRegistry; +import lol.pyr.znpcsplus.api.npc.NpcType; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.hologram.HologramItem; +import lol.pyr.znpcsplus.hologram.HologramLine; +import lol.pyr.znpcsplus.hologram.HologramText; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.serialization.NpcSerializerRegistryImpl; +import lol.pyr.znpcsplus.storage.NpcStorage; +import lol.pyr.znpcsplus.storage.NpcStorageType; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.World; + +import java.util.*; +import java.util.stream.Collectors; + +public class NpcRegistryImpl implements NpcRegistry { + private NpcStorage storage; + private final PacketFactory packetFactory; + private final ConfigManager configManager; + private final LegacyComponentSerializer textSerializer; + private final EntityPropertyRegistryImpl propertyRegistry; + + private final List npcList = new ArrayList<>(); + private final Map npcIdLookupMap = new HashMap<>(); + private final Map npcUuidLookupMap = new HashMap<>(); + + public NpcRegistryImpl(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistryImpl actionRegistry, TaskScheduler scheduler, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, NpcSerializerRegistryImpl serializerRegistry, LegacyComponentSerializer textSerializer) { + this.textSerializer = textSerializer; + this.propertyRegistry = propertyRegistry; + storage = configManager.getConfig().storageType().create(configManager, plugin, packetFactory, actionRegistry, typeRegistry, propertyRegistry, textSerializer, serializerRegistry); + if (storage == null) { + Bukkit.getLogger().warning("Failed to initialize storage, falling back to YAML"); + storage = NpcStorageType.YAML.create(configManager, plugin, packetFactory, actionRegistry, typeRegistry, propertyRegistry, textSerializer, serializerRegistry); + } + this.packetFactory = packetFactory; + this.configManager = configManager; + + if (configManager.getConfig().autoSaveEnabled()) { + long delay = configManager.getConfig().autoSaveInterval() * 20L; + scheduler.runDelayedTimerAsync(this::save, delay, delay); + } + } + + @Override + public void register(NpcEntry entry) { + register((NpcEntryImpl) entry); + } + + private void register(NpcEntryImpl entry) { + if (entry == null) throw new NullPointerException(); + unregister(npcIdLookupMap.put(entry.getId(), entry)); + unregister(npcUuidLookupMap.put(entry.getNpc().getUuid(), entry)); + npcList.add(entry); + } + + private void unregister(NpcEntryImpl entry) { + if (entry == null) return; + npcList.remove(entry); + NpcImpl one = npcIdLookupMap.remove(entry.getId()).getNpc(); + NpcImpl two = npcUuidLookupMap.remove(entry.getNpc().getUuid()).getNpc(); + if (one != null) one.delete(); + if (two != null && !Objects.equals(one, two)) two.delete(); + } + + private void unregisterAll() { + for (NpcEntryImpl entry : getAll()) { + if (entry.isSave()) entry.getNpc().delete(); + } + npcList.clear(); + npcIdLookupMap.clear(); + npcUuidLookupMap.clear(); + } + + public void registerAll(Collection entries) { + for (NpcEntryImpl entry : entries) register(entry); + } + + public void reload() { + unregisterAll(); + registerAll(storage.loadNpcs()); + } + + public void save() { + storage.saveNpcs(npcList.stream().filter(NpcEntryImpl::isSave).collect(Collectors.toList())); + } + + @Override + public NpcEntryImpl getById(String id) { + return npcIdLookupMap.get(id.toLowerCase()); + } + + @Override + public NpcEntry getByUuid(UUID uuid) { + return npcUuidLookupMap.get(uuid); + } + + public Collection getAll() { + return Collections.unmodifiableCollection(npcList); + } + + public Collection getProcessable() { + return Collections.unmodifiableCollection(npcList.stream() + .filter(NpcEntryImpl::isProcessed) + .collect(Collectors.toList())); + } + + public Collection getAllModifiable() { + return Collections.unmodifiableCollection(npcList.stream() + .filter(NpcEntryImpl::isAllowCommandModification) + .collect(Collectors.toList())); + } + + public NpcEntryImpl getByEntityId(int id) { + return npcList.stream().filter(entry -> entry.getNpc().getEntity().getEntityId() == id || + entry.getNpc().getHologram().getLines().stream().anyMatch(line -> line.getEntityId() == id)) // Also match the holograms of npcs + .findFirst().orElse(null); + } + + public Collection getAllIds() { + return Collections.unmodifiableSet(npcIdLookupMap.keySet()); + } + + @Override + public Collection getAllPlayerMade() { + return getAllModifiable(); + } + + @Override + public Collection getAllPlayerMadeIds() { + return getAllModifiable().stream() + .map(NpcEntryImpl::getId) + .collect(Collectors.toSet()); + } + + public Collection getModifiableIds() { + return Collections.unmodifiableSet(npcIdLookupMap.entrySet().stream() + .filter(entry -> entry.getValue().isAllowCommandModification()) + .map(Map.Entry::getKey) + .collect(Collectors.toSet())); + } + + public NpcEntryImpl create(String id, World world, NpcType type, NpcLocation location) { + return create(id, world, (NpcTypeImpl) type, location); + } + + public NpcEntryImpl create(String id, World world, NpcTypeImpl type, NpcLocation location) { + id = id.toLowerCase(); + if (npcIdLookupMap.containsKey(id)) throw new IllegalArgumentException("An npc with the id " + id + " already exists!"); + NpcImpl npc = new NpcImpl(UUID.randomUUID(), propertyRegistry, configManager, textSerializer, world, type, location, packetFactory); + type.applyDefaultProperties(npc); + NpcEntryImpl entry = new NpcEntryImpl(id, npc); + register(entry); + return entry; + } + + public NpcEntryImpl clone(String id, String newId, World newWorld, NpcLocation newLocation) { + NpcEntryImpl oldNpc = getById(id); + if (oldNpc == null) return null; + NpcEntryImpl newNpc = create(newId, newWorld, oldNpc.getNpc().getType(), newLocation); + newNpc.enableEverything(); + + for (EntityProperty property : oldNpc.getNpc().getAllProperties()) { + newNpc.getNpc().UNSAFE_setProperty(property, oldNpc.getNpc().getProperty(property)); + } + + for (InteractionAction action : oldNpc.getNpc().getActions()) { + newNpc.getNpc().addAction(action); + } + + for (HologramLine line : oldNpc.getNpc().getHologram().getLines()) { + if (line instanceof HologramText) { + HologramText text = (HologramText) line; + newNpc.getNpc().getHologram().addTextLineComponent(text.getValue()); + } + else if (line instanceof HologramItem) { + HologramItem item = (HologramItem) line; + newNpc.getNpc().getHologram().addItemLinePEStack(item.getValue()); + } + else throw new IllegalArgumentException("Unknown hologram line type during clone"); + } + + return newNpc; + } + + @Override + public void delete(String id) { + NpcEntryImpl entry = npcIdLookupMap.get(id.toLowerCase()); + if (entry == null) return; + unregister(entry); + storage.deleteNpc(entry); + } + + @Override + public void delete(UUID uuid) { + NpcEntryImpl entry = npcUuidLookupMap.get(uuid); + if (entry == null) return; + unregister(entry); + storage.deleteNpc(entry); + } + + public void switchIds(String oldId, String newId) { + NpcEntryImpl entry = getById(oldId); + delete(oldId); + NpcEntryImpl newEntry = new NpcEntryImpl(newId, entry.getNpc()); + newEntry.setSave(entry.isSave()); + newEntry.setProcessed(entry.isProcessed()); + newEntry.setAllowCommandModification(entry.isAllowCommandModification()); + register(newEntry); + } + + public void unload() { + npcList.forEach(npcEntry -> npcEntry.getNpc().delete()); + storage.close(); + } + + public NpcStorage getStorage() { + return storage; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeImpl.java new file mode 100644 index 0000000..a599306 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeImpl.java @@ -0,0 +1,217 @@ +package lol.pyr.znpcsplus.npc; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.npc.NpcType; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; + +import java.util.*; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class NpcTypeImpl implements NpcType { + private final EntityType type; + private final Set> allowedProperties; + private final Map, Object> defaultProperties; + private final String name; + private final double hologramOffset; + + private NpcTypeImpl(String name, EntityType type, double hologramOffset, Set> allowedProperties, Map, Object> defaultProperties) { + this.name = name.toLowerCase(); + this.type = type; + this.hologramOffset = hologramOffset; + this.allowedProperties = allowedProperties; + this.defaultProperties = defaultProperties; + } + + public String getName() { + return name; + } + + public EntityType getType() { + return type; + } + + public double getHologramOffset() { + return hologramOffset; + } + + public Set> getAllowedProperties() { + return allowedProperties.stream().map(property -> (EntityProperty) property).collect(Collectors.toSet()); + } + + public void applyDefaultProperties(NpcImpl npc) { + for (Map.Entry, Object> entry : defaultProperties.entrySet()) { + npc.UNSAFE_setProperty(entry.getKey(), entry.getValue()); + } + } + + public boolean isAllowedProperty(EntityPropertyImpl entityProperty) { + return !entityProperty.isPlayerModifiable() || allowedProperties.contains(entityProperty); + } + + protected static final class Builder { + private final static Logger logger = Logger.getLogger("NpcTypeBuilder"); + + private final EntityPropertyRegistryImpl propertyRegistry; + private final String name; + private final EntityType type; + private final List> allowedProperties = new ArrayList<>(); + private final Map, Object> defaultProperties = new HashMap<>(); + private double hologramOffset = 0; + + Builder(EntityPropertyRegistryImpl propertyRegistry, String name, EntityType type) { + this.propertyRegistry = propertyRegistry; + this.name = name; + this.type = type; + } + + public Builder addEquipmentProperties() { + addProperties("helmet", "chestplate", "leggings", "boots"); + return addHandProperties(); + } + + public Builder addHandProperties() { + if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_9)) { + return addProperties("hand", "offhand"); + } else { + return addProperties("hand"); + } + } + + public Builder addProperties(String... names) { + for (String name : names) { + if (propertyRegistry.getByName(name) == null) { + // Only for use in development, please comment this out in production because some properties are version-dependent + // logger.warning("Tried to register the non-existent \"" + name + "\" property to the \"" + this.name + "\" npc type"); + continue; + } + allowedProperties.add(propertyRegistry.getByName(name)); + } + return this; + } + + @SuppressWarnings("unchecked") + public Builder addDefaultProperty(String name, T value) { + EntityPropertyImpl property = (EntityPropertyImpl) propertyRegistry.getByName(name); + if (property == null) { + // Only for use in development, please comment this out in production because some properties are version-dependent + // logger.warning("Tried to register the non-existent \"" + name + "\" default property to the \"" + this.name + "\" npc type"); + return this; + } + defaultProperties.put(property, value); + return this; + } + + public Builder setHologramOffset(double hologramOffset) { + this.hologramOffset = hologramOffset; + return this; + } + + public NpcTypeImpl build() { + ServerVersion version = PacketEvents.getAPI().getServerManager().getVersion(); + addProperties("fire", "invisible", "silent", "look", "look_distance", "look_return", "view_distance", + "potion_color", "potion_ambient", "display_name", "permission_required", + "player_knockback", "player_knockback_exempt_permission", "player_knockback_distance", "player_knockback_vertical", + "player_knockback_horizontal", "player_knockback_cooldown", "player_knockback_sound", "player_knockback_sound_name", + "player_knockback_sound_volume", "player_knockback_sound_pitch"); + if (!type.equals(EntityTypes.PLAYER)) addProperties("dinnerbone"); + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.LIVINGENTITY)) { + addProperties("health", "attribute_max_health"); + } + // TODO: make this look nicer after completing the rest of the properties + if (version.isNewerThanOrEquals(ServerVersion.V_1_9)) addProperties("glow"); + if (version.isNewerThanOrEquals(ServerVersion.V_1_14)) { + addProperties("pose"); + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.HORSE)) { + addProperties("chestplate"); + } + } + if (version.isNewerThanOrEquals(ServerVersion.V_1_17)) addProperties("shaking"); + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.ABSTRACT_AGEABLE) || EntityTypes.isTypeInstanceOf(type, EntityTypes.ZOMBIE) || EntityTypes.isTypeInstanceOf(type, EntityTypes.ZOGLIN)) { + addProperties("baby"); + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.ABSTRACT_HORSE)) { + addProperties("is_saddled", "is_eating", "is_rearing", "has_mouth_open"); + } + if (type.equals(EntityTypes.HORSE) && version.isOlderThan(ServerVersion.V_1_14)) { + addProperties("horse_armor"); + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.CHESTED_HORSE)) { + addProperties("has_chest"); + } else if (version.isOlderThan(ServerVersion.V_1_11) && type.equals(EntityTypes.HORSE)) { + addProperties("has_chest"); + } + if (version.isOlderThan(ServerVersion.V_1_11) && EntityTypes.isTypeInstanceOf(type, EntityTypes.SKELETON)) { + addProperties("skeleton_type"); + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.ABSTRACT_EVO_ILLU_ILLAGER)) { + addProperties("spell"); + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.ABSTRACT_PIGLIN)) { + addProperties("piglin_immune_to_zombification"); + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.SLIME) || EntityTypes.isTypeInstanceOf(type, EntityTypes.PHANTOM)) { + addProperties("size"); + } + if (version.isOlderThan(ServerVersion.V_1_14)) { + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.OCELOT)) { + addProperties("ocelot_type"); + } + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.PANDA)) { + if (version.isNewerThanOrEquals(ServerVersion.V_1_15)) { + addProperties("panda_rolling", "panda_sitting", "panda_on_back", "hand"); + } else { + addProperties("panda_eating"); + } + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.ABSTRACT_TAMEABLE_ANIMAL) && + !(version.isNewerThanOrEquals(ServerVersion.V_1_14) && type.equals(EntityTypes.OCELOT))) { + addProperties("tamed", "sitting"); + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.GUARDIAN)) { + addProperties("is_retracting_spikes"); + } + if (version.isNewerThanOrEquals(ServerVersion.V_1_20_5)) { + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.WOLF)) { + addProperties("wolf_variant"); + if (version.isNewerThanOrEquals(ServerVersion.V_1_21)) { + addProperties("body"); + } else { + addProperties("chestplate"); + } + } + } + if (version.isNewerThanOrEquals(ServerVersion.V_1_21_4)) { + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.CREAKING)) { + addProperties("creaking_crumbling"); + } + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.ZOMBIE)) { + if (version.isOlderThan(ServerVersion.V_1_9)) { + addProperties("zombie_is_villager"); + } else if (version.isOlderThan(ServerVersion.V_1_11)) { + addProperties("zombie_type"); + } + + if (version.isOlderThan(ServerVersion.V_1_11)) { + addProperties("is_converting"); + } + + if (version.isNewerThanOrEquals(ServerVersion.V_1_9) && version.isOlderThan(ServerVersion.V_1_14)) { + addProperties("zombie_hands_held_up"); + } + + if (version.isNewerThanOrEquals(ServerVersion.V_1_13)) { + addProperties("zombie_becoming_drowned"); + } + } + return new NpcTypeImpl(name, type, hologramOffset, new HashSet<>(allowedProperties), defaultProperties); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeRegistryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeRegistryImpl.java new file mode 100644 index 0000000..8b877a3 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeRegistryImpl.java @@ -0,0 +1,423 @@ +package lol.pyr.znpcsplus.npc; + +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import lol.pyr.znpcsplus.api.npc.NpcType; +import lol.pyr.znpcsplus.api.npc.NpcTypeRegistry; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import org.bukkit.plugin.Plugin; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class NpcTypeRegistryImpl implements NpcTypeRegistry { + private final List types = new ArrayList<>(); + + private NpcTypeImpl register(NpcTypeImpl.Builder builder) { + return register(builder.build()); + } + + private NpcTypeImpl register(NpcTypeImpl type) { + types.add(type); + return type; + } + + private NpcTypeImpl.Builder builder(EntityPropertyRegistryImpl propertyRegistry, String name, EntityType type) { + return new NpcTypeImpl.Builder(propertyRegistry, name, type); + } + + public void registerDefault(PacketEventsAPI packetEvents, EntityPropertyRegistryImpl p /* propertyRegistry */) { + ServerVersion version = packetEvents.getServerManager().getVersion(); + + register(builder(p, "player", EntityTypes.PLAYER) + .setHologramOffset(-0.15D) + .addEquipmentProperties() + .addProperties("skin_cape", "skin_jacket", "skin_left_sleeve", "skin_right_sleeve", "skin_left_leg", "skin_right_leg", "skin_hat", "shoulder_entity_left", "shoulder_entity_right", "force_body_rotation", "entity_sitting") + .addDefaultProperty("skin_cape", true) + .addDefaultProperty("skin_jacket", true) + .addDefaultProperty("skin_left_sleeve", true) + .addDefaultProperty("skin_right_sleeve", true) + .addDefaultProperty("skin_left_leg", true) + .addDefaultProperty("skin_right_leg", true) + .addDefaultProperty("skin_hat", true)); + + // Most hologram offsets generated using Entity#getHeight() in 1.19.4 + + register(builder(p, "armor_stand", EntityTypes.ARMOR_STAND) + .setHologramOffset(0) + .addEquipmentProperties() + .addProperties("small", "arms", "base_plate", "head_rotation", "body_rotation", "left_arm_rotation", "right_arm_rotation", "left_leg_rotation", "right_leg_rotation")); + + register(builder(p, "bat", EntityTypes.BAT) + .setHologramOffset(-1.075) + .addProperties("hanging")); + + register(builder(p, "blaze", EntityTypes.BLAZE) + .setHologramOffset(-0.175) + .addProperties("blaze_on_fire")); + + register(builder(p, "cave_spider", EntityTypes.CAVE_SPIDER) + .setHologramOffset(-1.475)); + + register(builder(p, "chicken", EntityTypes.CHICKEN) + .setHologramOffset(-1.275)); + + register(builder(p, "cow", EntityTypes.COW) + .setHologramOffset(-0.575)); + + register(builder(p, "creeper", EntityTypes.CREEPER) + .setHologramOffset(-0.275) + .addProperties("creeper_state", "creeper_charged")); + + register(builder(p, "end_crystal", EntityTypes.END_CRYSTAL) + .setHologramOffset(0.025) + .addProperties("beam_target", "show_base")); + + register(builder(p, "ender_dragon", EntityTypes.ENDER_DRAGON) + .setHologramOffset(6.0245)); + + register(builder(p, "enderman", EntityTypes.ENDERMAN) + .setHologramOffset(0.925) + .addProperties("enderman_held_block", "enderman_screaming", "enderman_staring", "entity_sitting")); + + register(builder(p, "endermite", EntityTypes.ENDERMITE) + .setHologramOffset(-1.675)); + + register(builder(p, "ghast", EntityTypes.GHAST) + .setHologramOffset(2.025) + .addProperties("attacking")); + + register(builder(p, "giant", EntityTypes.GIANT) + .setHologramOffset(10.025) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "guardian", EntityTypes.GUARDIAN) + .setHologramOffset(-1.125) + .addProperties("is_elder")); + + register(builder(p, "horse", EntityTypes.HORSE) + .setHologramOffset(-0.375) + .addProperties("horse_type", "horse_style", "horse_color", "horse_armor")); + + register(builder(p, "iron_golem", EntityTypes.IRON_GOLEM) + .setHologramOffset(0.725)); + + register(builder(p, "magma_cube", EntityTypes.MAGMA_CUBE) + .setHologramOffset(-1.455)); // TODO: Hologram offset scaling with size property + + register(builder(p, "mooshroom", EntityTypes.MOOSHROOM) + .setHologramOffset(-0.575) + .addProperties("mooshroom_variant")); + + register(builder(p, "ocelot", EntityTypes.OCELOT) + .setHologramOffset(-1.275)); + + register(builder(p, "pig", EntityTypes.PIG) + .setHologramOffset(-1.075) + .addProperties("pig_saddled")); + + register(builder(p, "rabbit", EntityTypes.RABBIT) + .setHologramOffset(-1.475) + .addProperties("rabbit_type")); + + register(builder(p, "sheep", EntityTypes.SHEEP) + .setHologramOffset(-0.675) + .addProperties("sheep_color", "sheep_sheared")); + + register(builder(p, "silverfish", EntityTypes.SILVERFISH) + .setHologramOffset(-1.675)); + + register(builder(p, "skeleton", EntityTypes.SKELETON) + .setHologramOffset(0.015) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "skeleton_horse", EntityTypes.SKELETON_HORSE) + .setHologramOffset(-0.375)); + + register(builder(p, "slime", EntityTypes.SLIME) + .setHologramOffset(-1.455)); // TODO: Hologram offset scaling with size property + + register(builder(p, "snow_golem", EntityTypes.SNOW_GOLEM) + .setHologramOffset(-0.075) + .addProperties("derpy_snowgolem")); + + register(builder(p, "spider", EntityTypes.SPIDER) + .setHologramOffset(-1.075)); + + register(builder(p, "squid", EntityTypes.SQUID) + .setHologramOffset(-1.175)); + + register(builder(p, "villager", EntityTypes.VILLAGER) + .setHologramOffset(-0.025) + .addProperties("hand", "villager_type", "villager_profession", "villager_level")); + + register(builder(p, "witch", EntityTypes.WITCH) + .setHologramOffset(-0.025) + .addProperties("hand")); + + register(builder(p, "wither", EntityTypes.WITHER) + .setHologramOffset(1.525) + .addProperties("invulnerable_time")); + + register(builder(p, "wolf", EntityTypes.WOLF) + .setHologramOffset(-1.125) + .addProperties("wolf_begging", "wolf_collar", "wolf_angry")); + + register(builder(p, "zombie", EntityTypes.ZOMBIE) + .setHologramOffset(-0.025) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "zombie_horse", EntityTypes.ZOMBIE_HORSE) + .setHologramOffset(-0.375)); + + register(builder(p, "zombified_piglin", EntityTypes.ZOMBIFIED_PIGLIN) + .setHologramOffset(-0.025) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_9)) return; + + register(builder(p, "shulker", EntityTypes.SHULKER) + .setHologramOffset(-0.975) + .addProperties("attach_direction", "shield_height", "shulker_color")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_10)) return; + + register(builder(p, "polar_bear", EntityTypes.POLAR_BEAR) + .setHologramOffset(-0.575) + .addProperties("polar_bear_standing")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_11)) return; + + register(builder(p, "donkey", EntityTypes.DONKEY) + .setHologramOffset(-0.475)); + + register(builder(p, "mule", EntityTypes.MULE) + .setHologramOffset(-0.375)); + + register(builder(p, "elder_guardian", EntityTypes.ELDER_GUARDIAN) + .setHologramOffset(0.0225)); + + register(builder(p, "husk", EntityTypes.HUSK) + .setHologramOffset(-0.025) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "stray", EntityTypes.STRAY) + .setHologramOffset(0.015) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "evoker", EntityTypes.EVOKER) + .setHologramOffset(-0.025) + .addProperties("entity_sitting")); + + register(builder(p, "llama", EntityTypes.LLAMA) + .setHologramOffset(-0.105) + .addProperties("carpet_color", "llama_variant", "body")); + + register(builder(p, "vex", EntityTypes.VEX) + .setHologramOffset(-1.175) + .addHandProperties()); + + register(builder(p, "vindicator", EntityTypes.VINDICATOR) + .setHologramOffset(-0.025) + .addProperties("celebrating", "entity_sitting")); + + register(builder(p, "wither_skeleton", EntityTypes.WITHER_SKELETON) + .setHologramOffset(0.425) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "zombie_villager", EntityTypes.ZOMBIE_VILLAGER) + .setHologramOffset(-0.025) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_12)) return; + + register(builder(p, "illusioner", EntityTypes.ILLUSIONER) + .setHologramOffset(-0.025) + .addProperties("entity_sitting")); + + register(builder(p, "parrot", EntityTypes.PARROT) + .setHologramOffset(-1.075) + .addProperties("parrot_variant")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_13)) return; + + register(builder(p, "cod", EntityTypes.COD) + .setHologramOffset(-1.675)); + + register(builder(p, "dolphin", EntityTypes.DOLPHIN) + .setHologramOffset(-1.375) + .addProperties("hand")); + + register(builder(p, "drowned", EntityTypes.DROWNED) + .setHologramOffset(-0.025) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "phantom", EntityTypes.PHANTOM) + .setHologramOffset(-1.475)); + + register(builder(p, "pufferfish", EntityTypes.PUFFERFISH) + .setHologramOffset(-1.625) + .addProperties("puff_state")); + + register(builder(p, "salmon", EntityTypes.SALMON) + .setHologramOffset(-1.575)); + + register(builder(p, "tropical_fish", EntityTypes.TROPICAL_FISH) + .setHologramOffset(-1.575) + .addProperties("tropical_fish_pattern", "tropical_fish_body_color", "tropical_fish_pattern_color")); + + register(builder(p, "turtle", EntityTypes.TURTLE) + .setHologramOffset(-1.575)); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_14)) return; + + register(builder(p, "cat", EntityTypes.CAT) + .setHologramOffset(-1.275) + .addProperties("cat_variant", "cat_laying", "cat_relaxed", "cat_collar")); + + register(builder(p, "fox", EntityTypes.FOX) + .setHologramOffset(-1.275) + .addProperties("hand", "fox_variant", "fox_sitting", "fox_crouching", "fox_sleeping", "fox_faceplanted")); + + register(builder(p, "panda", EntityTypes.PANDA) + .setHologramOffset(-0.725) + .addProperties("panda_main_gene", "panda_hidden_gene", "panda_sneezing")); + + register(builder(p, "pillager", EntityTypes.PILLAGER) + .setHologramOffset(-0.025) + .addHandProperties() + .addProperties("pillager_charging", "entity_sitting")); + + register(builder(p, "ravager", EntityTypes.RAVAGER) + .setHologramOffset(0.225)); + + register(builder(p, "trader_llama", EntityTypes.TRADER_LLAMA) + .setHologramOffset(-0.105) + .addProperties("carpet_color", "llama_variant", "body")); + + register(builder(p, "wandering_trader", EntityTypes.WANDERING_TRADER) + .setHologramOffset(-0.025) + .addProperties("hand")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_15)) return; + + register(builder(p, "bee", EntityTypes.BEE) + .setHologramOffset(-1.375) + .addProperties("angry", "has_nectar")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_16)) return; + + register(builder(p, "hoglin", EntityTypes.HOGLIN) + .setHologramOffset(-0.575) + .addProperties("hoglin_immune_to_zombification")); + + register(builder(p, "piglin", EntityTypes.PIGLIN) + .setHologramOffset(-0.025) + .addEquipmentProperties() + .addProperties("piglin_baby", "piglin_charging_crossbow", "piglin_dancing", "entity_sitting")); + + register(builder(p, "piglin_brute", EntityTypes.PIGLIN_BRUTE) + .setHologramOffset(-0.025) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "strider", EntityTypes.STRIDER) + .setHologramOffset(-0.275) + .addProperties("strider_shaking", "strider_saddled")); + + register(builder(p, "zoglin", EntityTypes.ZOGLIN) + .setHologramOffset(-0.575)); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_17)) return; + + register(builder(p, "axolotl", EntityTypes.AXOLOTL) + .setHologramOffset(-1.555) + .addProperties("axolotl_variant", "playing_dead")); + + register(builder(p, "glow_squid", EntityTypes.GLOW_SQUID) + .setHologramOffset(-1.175)); + + register(builder(p, "goat", EntityTypes.GOAT) + .setHologramOffset(-0.675) + .addProperties("has_left_horn", "has_right_horn")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_19)) return; + + register(builder(p, "allay", EntityTypes.ALLAY) + .setHologramOffset(-1.375) + .addHandProperties()); + + register(builder(p, "frog", EntityTypes.FROG) + .setHologramOffset(-1.475) + .addProperties("frog_variant", "frog_target_npc")); + + register(builder(p, "tadpole", EntityTypes.TADPOLE) + .setHologramOffset(-1.675)); + + register(builder(p, "warden", EntityTypes.WARDEN) + .setHologramOffset(0.925) + .addProperties("warden_anger")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_20)) return; + + register(builder(p, "sniffer", EntityTypes.SNIFFER) + .setHologramOffset(0.075) + .addProperties("sniffer_state")); + + register(builder(p, "camel", EntityTypes.CAMEL) + .setHologramOffset(0.4) + .addProperties("bashing", "camel_sitting")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_20_5)) return; + + register(builder(p, "armadillo", EntityTypes.ARMADILLO) + .setHologramOffset(-1.325) + .addProperties("armadillo_state")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_21)) return; + + register(builder(p, "bogged", EntityTypes.BOGGED) + .setHologramOffset(0.015) + .addProperties("bogged_sheared", "entity_sitting")); + + register(builder(p, "breeze", EntityTypes.BREEZE) + .setHologramOffset(-0.205)); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_21_2)) return; + + register(builder(p, "creaking", EntityTypes.CREAKING) + .setHologramOffset(0.725) + .addProperties("creaking_active")); + } + + public Collection getAll() { + return Collections.unmodifiableList(types); + } + + public Collection getAllImpl() { + return Collections.unmodifiableList(types); + } + + public NpcTypeImpl getByName(String name) { + for (NpcTypeImpl type : types) if (type.getName().equalsIgnoreCase(name)) return type; + return null; + } + + public NpcTypeImpl getByEntityType(EntityType entityType) { + for (NpcTypeImpl type : types) if (type.getType() == entityType) return type; + return null; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/packets/PacketFactory.java b/plugin/src/main/java/lol/pyr/znpcsplus/packets/PacketFactory.java new file mode 100644 index 0000000..7cfdc93 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/packets/PacketFactory.java @@ -0,0 +1,31 @@ +package lol.pyr.znpcsplus.packets; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.player.Equipment; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUpdateAttributes; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.NamedColor; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public interface PacketFactory { + CompletableFuture spawnPlayer(Player player, PacketEntity entity, PropertyHolder properties); + void spawnEntity(Player player, PacketEntity entity, PropertyHolder properties); + void destroyEntity(Player player, PacketEntity entity, PropertyHolder properties); + void teleportEntity(Player player, PacketEntity entity); + CompletableFuture addTabPlayer(Player player, PacketEntity entity, PropertyHolder properties); + void removeTabPlayer(Player player, PacketEntity entity); + void createTeam(Player player, PacketEntity entity, NamedColor namedColor); + void removeTeam(Player player, PacketEntity entity); + void sendAllMetadata(Player player, PacketEntity entity, PropertyHolder properties); + void sendEquipment(Player player, PacketEntity entity, Equipment equipment); + void sendMetadata(Player player, PacketEntity entity, List> data); + void sendHeadRotation(Player player, PacketEntity entity, float yaw, float pitch); + void sendHandSwing(Player player, PacketEntity entity, boolean offHand); + void setPassengers(Player player, int vehicle, int... passengers); + void sendAllAttributes(Player player, PacketEntity entity, PropertyHolder properties); + void sendAttribute(Player player, PacketEntity entity, WrapperPlayServerUpdateAttributes.Property property); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_17PacketFactory.java b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_17PacketFactory.java new file mode 100644 index 0000000..5a97fbf --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_17PacketFactory.java @@ -0,0 +1,34 @@ +package lol.pyr.znpcsplus.packets; + +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import com.github.retrooper.packetevents.util.Vector3d; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnEntity; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.util.NamedColor; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.util.Optional; + +public class V1_17PacketFactory extends V1_8PacketFactory { + public V1_17PacketFactory(TaskScheduler scheduler, PacketEventsAPI packetEvents, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, ConfigManager configManager) { + super(scheduler, packetEvents, propertyRegistry, textSerializer, configManager); + } + + @Override + public void spawnEntity(Player player, PacketEntity entity, PropertyHolder properties) { + NpcLocation location = entity.getLocation(); + sendPacket(player, new WrapperPlayServerSpawnEntity(entity.getEntityId(), Optional.of(entity.getUuid()), entity.getType(), + npcLocationToVector(location), location.getPitch(), location.getYaw(), location.getYaw(), 0, Optional.of(new Vector3d()))); + sendAllMetadata(player, entity, properties); + if (EntityTypes.isTypeInstanceOf(entity.getType(), EntityTypes.LIVINGENTITY)) sendAllAttributes(player, entity, properties); + createTeam(player, entity, properties.getProperty(propertyRegistry.getByName("glow", NamedColor.class))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_19_3PacketFactory.java b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_19_3PacketFactory.java new file mode 100644 index 0000000..22fe62d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_19_3PacketFactory.java @@ -0,0 +1,47 @@ +package lol.pyr.znpcsplus.packets; + +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import com.github.retrooper.packetevents.protocol.player.GameMode; +import com.github.retrooper.packetevents.protocol.player.UserProfile; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPlayerInfoRemove; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPlayerInfoUpdate; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.util.EnumSet; +import java.util.concurrent.CompletableFuture; + +public class V1_19_3PacketFactory extends V1_17PacketFactory { + public V1_19_3PacketFactory(TaskScheduler scheduler, PacketEventsAPI packetEvents, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, ConfigManager configManager) { + super(scheduler, packetEvents, propertyRegistry, textSerializer, configManager); + } + + @Override + public CompletableFuture addTabPlayer(Player player, PacketEntity entity, PropertyHolder properties) { + if (entity.getType() != EntityTypes.PLAYER) return CompletableFuture.completedFuture(null); + CompletableFuture future = new CompletableFuture<>(); + skinned(player, properties, new UserProfile(entity.getUuid(), Integer.toString(entity.getEntityId()))).thenAccept(profile -> { + WrapperPlayServerPlayerInfoUpdate.PlayerInfo info = new WrapperPlayServerPlayerInfoUpdate.PlayerInfo( + profile, false, 1, GameMode.CREATIVE, + Component.text(configManager.getConfig().tabDisplayName().replace("{id}", Integer.toString(entity.getEntityId()))), null); + sendPacket(player, new WrapperPlayServerPlayerInfoUpdate(EnumSet.of(WrapperPlayServerPlayerInfoUpdate.Action.ADD_PLAYER, + WrapperPlayServerPlayerInfoUpdate.Action.UPDATE_LISTED), info, info)); + future.complete(null); + }); + return future; + } + + @Override + public void removeTabPlayer(Player player, PacketEntity entity) { + if (entity.getType() != EntityTypes.PLAYER) return; + sendPacket(player, new WrapperPlayServerPlayerInfoRemove(entity.getUuid())); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_20_2PacketFactory.java b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_20_2PacketFactory.java new file mode 100644 index 0000000..4a760e4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_20_2PacketFactory.java @@ -0,0 +1,43 @@ +package lol.pyr.znpcsplus.packets; + +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.util.Vector3d; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityHeadLook; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnEntity; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.util.NamedColor; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public class V1_20_2PacketFactory extends V1_19_3PacketFactory { + + protected ConfigManager configManager; + + public V1_20_2PacketFactory(TaskScheduler scheduler, PacketEventsAPI packetEvents, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, ConfigManager configManager) { + super(scheduler, packetEvents, propertyRegistry, textSerializer, configManager); + this.configManager = configManager; + } + + @Override + public CompletableFuture spawnPlayer(Player player, PacketEntity entity, PropertyHolder properties) { + return addTabPlayer(player, entity, properties).thenAccept(ignored -> { + createTeam(player, entity, properties.getProperty(propertyRegistry.getByName("glow", NamedColor.class))); + NpcLocation location = entity.getLocation(); + sendPacket(player, new WrapperPlayServerSpawnEntity(entity.getEntityId(), Optional.of(entity.getUuid()), entity.getType(), + npcLocationToVector(location), location.getPitch(), location.getYaw(), location.getYaw(), 0, Optional.of(new Vector3d()))); + sendPacket(player, new WrapperPlayServerEntityHeadLook(entity.getEntityId(), location.getYaw())); + sendAllMetadata(player, entity, properties); + sendAllAttributes(player, entity, properties); + scheduler.runLaterAsync(() -> removeTabPlayer(player, entity), configManager.getConfig().tabHideDelay()); + }); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_21_3PacketFactory.java b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_21_3PacketFactory.java new file mode 100644 index 0000000..7b7ddc5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_21_3PacketFactory.java @@ -0,0 +1,29 @@ +package lol.pyr.znpcsplus.packets; + +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.protocol.entity.EntityPositionData; +import com.github.retrooper.packetevents.protocol.teleport.RelativeFlag; +import com.github.retrooper.packetevents.util.Vector3d; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityHeadLook; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityTeleport; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +public class V1_21_3PacketFactory extends V1_20_2PacketFactory { + public V1_21_3PacketFactory(TaskScheduler scheduler, PacketEventsAPI packetEvents, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, ConfigManager configManager) { + super(scheduler, packetEvents, propertyRegistry, textSerializer, configManager); + } + + @Override + public void teleportEntity(Player player, PacketEntity entity) { + NpcLocation location = entity.getLocation(); + sendPacket(player, new WrapperPlayServerEntityTeleport(entity.getEntityId(), new EntityPositionData(npcLocationToVector(location), new Vector3d(0, 0, 0), location.getYaw(), location.getPitch()), RelativeFlag.NONE, false)); + sendPacket(player, new WrapperPlayServerEntityHeadLook(entity.getEntityId(), location.getYaw())); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_21_7PacketFactory.java b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_21_7PacketFactory.java new file mode 100644 index 0000000..934a2ce --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_21_7PacketFactory.java @@ -0,0 +1,14 @@ +package lol.pyr.znpcsplus.packets; + +import com.github.retrooper.packetevents.PacketEventsAPI; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.plugin.Plugin; + +public class V1_21_7PacketFactory extends V1_21_3PacketFactory { + public V1_21_7PacketFactory(TaskScheduler scheduler, PacketEventsAPI packetEvents, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, ConfigManager configManager) { + super(scheduler, packetEvents, propertyRegistry, textSerializer, configManager); + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_8PacketFactory.java b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_8PacketFactory.java new file mode 100644 index 0000000..085f338 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_8PacketFactory.java @@ -0,0 +1,208 @@ +package lol.pyr.znpcsplus.packets; + +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import com.github.retrooper.packetevents.protocol.player.ClientVersion; +import com.github.retrooper.packetevents.protocol.player.Equipment; +import com.github.retrooper.packetevents.protocol.player.GameMode; +import com.github.retrooper.packetevents.protocol.player.UserProfile; +import com.github.retrooper.packetevents.util.Vector3d; +import com.github.retrooper.packetevents.wrapper.PacketWrapper; +import com.github.retrooper.packetevents.wrapper.play.server.*; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.entity.properties.attributes.AttributeProperty; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.skin.BaseSkinDescriptor; +import lol.pyr.znpcsplus.util.NamedColor; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +public class V1_8PacketFactory implements PacketFactory { + protected final TaskScheduler scheduler; + protected final PacketEventsAPI packetEvents; + protected final EntityPropertyRegistryImpl propertyRegistry; + protected final LegacyComponentSerializer textSerializer; + protected ConfigManager configManager; + + public V1_8PacketFactory(TaskScheduler scheduler, PacketEventsAPI packetEvents, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, ConfigManager configManager) { + this.scheduler = scheduler; + this.packetEvents = packetEvents; + this.propertyRegistry = propertyRegistry; + this.textSerializer = textSerializer; + this.configManager = configManager; + } + + @Override + public CompletableFuture spawnPlayer(Player player, PacketEntity entity, PropertyHolder properties) { + return addTabPlayer(player, entity, properties).thenAccept(ignored -> { + createTeam(player, entity, properties.getProperty(propertyRegistry.getByName("glow", NamedColor.class))); + NpcLocation location = entity.getLocation(); + sendPacket(player, new WrapperPlayServerSpawnPlayer(entity.getEntityId(), + entity.getUuid(), npcLocationToVector(location), location.getYaw(), location.getPitch(), Collections.emptyList())); + sendPacket(player, new WrapperPlayServerEntityHeadLook(entity.getEntityId(), location.getYaw())); + sendAllMetadata(player, entity, properties); + sendAllAttributes(player, entity, properties); + scheduler.runLaterAsync(() -> removeTabPlayer(player, entity), configManager.getConfig().tabHideDelay()); + }); + } + + @Override + public void spawnEntity(Player player, PacketEntity entity, PropertyHolder properties) { + NpcLocation location = entity.getLocation(); + EntityType type = entity.getType(); + ClientVersion clientVersion = packetEvents.getServerManager().getVersion().toClientVersion(); + sendPacket(player, type.getLegacyId(clientVersion) == -1 ? + new WrapperPlayServerSpawnLivingEntity(entity.getEntityId(), entity.getUuid(), type, npcLocationToVector(location), + location.getYaw(), location.getPitch(), location.getYaw(), new Vector3d(), Collections.emptyList()) : + new WrapperPlayServerSpawnEntity(entity.getEntityId(), Optional.of(entity.getUuid()), entity.getType(), npcLocationToVector(location), + location.getPitch(), location.getYaw(), location.getYaw(), 0, Optional.empty())); + sendAllMetadata(player, entity, properties); + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.LIVINGENTITY)) sendAllAttributes(player, entity, properties); + createTeam(player, entity, properties.getProperty(propertyRegistry.getByName("glow", NamedColor.class))); + } + + protected Vector3d npcLocationToVector(NpcLocation location) { + return new Vector3d(location.getX(), location.getY(), location.getZ()); + } + + @Override + public void destroyEntity(Player player, PacketEntity entity, PropertyHolder properties) { + sendPacket(player, new WrapperPlayServerDestroyEntities(entity.getEntityId())); + removeTeam(player, entity); + } + + @Override + public void teleportEntity(Player player, PacketEntity entity) { + NpcLocation location = entity.getLocation(); + sendPacket(player, new WrapperPlayServerEntityTeleport(entity.getEntityId(), npcLocationToVector(location), location.getYaw(), location.getPitch(), true)); + sendPacket(player, new WrapperPlayServerEntityHeadLook(entity.getEntityId(), location.getYaw())); + } + + @Override + public CompletableFuture addTabPlayer(Player player, PacketEntity entity, PropertyHolder properties) { + if (entity.getType() != EntityTypes.PLAYER) return CompletableFuture.completedFuture(null); + CompletableFuture future = new CompletableFuture<>(); + skinned(player, properties, new UserProfile(entity.getUuid(), Integer.toString(entity.getEntityId()))).thenAccept(profile -> { + sendPacket(player, new WrapperPlayServerPlayerInfo( + WrapperPlayServerPlayerInfo.Action.ADD_PLAYER, new WrapperPlayServerPlayerInfo.PlayerData( + Component.text(configManager.getConfig().tabDisplayName().replace("{id}", Integer.toString(entity.getEntityId()))), + profile, GameMode.CREATIVE, 1))); + future.complete(null); + }); + return future; + } + + @Override + public void removeTabPlayer(Player player, PacketEntity entity) { + if (entity.getType() != EntityTypes.PLAYER) return; + sendPacket(player, new WrapperPlayServerPlayerInfo( + WrapperPlayServerPlayerInfo.Action.REMOVE_PLAYER, new WrapperPlayServerPlayerInfo.PlayerData(null, + new UserProfile(entity.getUuid(), null), null, -1))); + } + + @Override + public void createTeam(Player player, PacketEntity entity, NamedColor namedColor) { + sendPacket(player, new WrapperPlayServerTeams("npc_team_" + entity.getEntityId(), WrapperPlayServerTeams.TeamMode.CREATE, new WrapperPlayServerTeams.ScoreBoardTeamInfo( + Component.text(" "), null, null, + WrapperPlayServerTeams.NameTagVisibility.NEVER, + WrapperPlayServerTeams.CollisionRule.NEVER, + namedColor == null ? NamedTextColor.WHITE : NamedTextColor.NAMES.value(namedColor.name().toLowerCase()), + WrapperPlayServerTeams.OptionData.NONE + ))); + sendPacket(player, new WrapperPlayServerTeams("npc_team_" + entity.getEntityId(), WrapperPlayServerTeams.TeamMode.ADD_ENTITIES, (WrapperPlayServerTeams.ScoreBoardTeamInfo) null, + entity.getType() == EntityTypes.PLAYER ? Integer.toString(entity.getEntityId()) : entity.getUuid().toString())); + } + + @Override + public void removeTeam(Player player, PacketEntity entity) { + sendPacket(player, new WrapperPlayServerTeams("npc_team_" + entity.getEntityId(), WrapperPlayServerTeams.TeamMode.REMOVE, (WrapperPlayServerTeams.ScoreBoardTeamInfo) null)); + } + + @Override + public void sendAllMetadata(Player player, PacketEntity entity, PropertyHolder properties) { + Map> datas = new HashMap<>(); + for (EntityProperty property : properties.getAppliedProperties()) ((EntityPropertyImpl) property).apply(player, entity, false, datas); + sendMetadata(player, entity, new ArrayList<>(datas.values())); + } + + @Override + public void sendMetadata(Player player, PacketEntity entity, List> data) { + sendPacket(player, new WrapperPlayServerEntityMetadata(entity.getEntityId(), data)); + } + + @Override + public void sendHeadRotation(Player player, PacketEntity entity, float yaw, float pitch) { + sendPacket(player, new WrapperPlayServerEntityHeadLook(entity.getEntityId(),yaw)); + sendPacket(player, new WrapperPlayServerEntityRotation(entity.getEntityId(), yaw, pitch, true)); + } + + @Override + public void sendEquipment(Player player, PacketEntity entity, Equipment equipment) { + sendPacket(player, new WrapperPlayServerEntityEquipment(entity.getEntityId(), Collections.singletonList(equipment))); + } + + @Override + public void setPassengers(Player player, int vehicleEntityId, int... passengers) { + sendPacket(player, new WrapperPlayServerSetPassengers(vehicleEntityId, passengers)); + } + + protected void sendPacket(Player player, PacketWrapper packet) { + packetEvents.getPlayerManager().sendPacket(player, packet); + } + + protected CompletableFuture skinned(Player player, PropertyHolder properties, UserProfile profile) { + if (!properties.hasProperty(propertyRegistry.getByName("skin"))) return CompletableFuture.completedFuture(profile); + BaseSkinDescriptor descriptor = (BaseSkinDescriptor) properties.getProperty(propertyRegistry.getByName("skin", SkinDescriptor.class)); + if (descriptor.supportsInstant(player)) { + descriptor.fetchInstant(player).apply(profile); + return CompletableFuture.completedFuture(profile); + } + CompletableFuture future = new CompletableFuture<>(); + descriptor.fetch(player).thenAccept(skin -> { + if (skin != null) skin.apply(profile); + future.complete(profile); + }); + return future; + } + + protected void add(Map> map, EntityData data) { + map.put(data.getIndex(), data); + } + + @Override + public void sendHandSwing(Player player, PacketEntity entity, boolean offHand) { + sendPacket(player, new WrapperPlayServerEntityAnimation(entity.getEntityId(), offHand ? + WrapperPlayServerEntityAnimation.EntityAnimationType.SWING_OFF_HAND : + WrapperPlayServerEntityAnimation.EntityAnimationType.SWING_MAIN_ARM)); + } + + @Override + public void sendAllAttributes(Player player, PacketEntity entity, PropertyHolder properties) { + List attributesList = new ArrayList<>(); + properties.getAppliedProperties() + .stream() + .filter(property -> property instanceof AttributeProperty) + .forEach(property -> ((AttributeProperty) property).apply(player, entity, false, attributesList)); + sendPacket(player, new WrapperPlayServerUpdateAttributes(entity.getEntityId(), attributesList)); + } + + @Override + public void sendAttribute(Player player, PacketEntity entity, WrapperPlayServerUpdateAttributes.Property property) { + sendPacket(player, new WrapperPlayServerUpdateAttributes(entity.getEntityId(), Collections.singletonList(property))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/ColorParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/ColorParser.java new file mode 100644 index 0000000..65d0d44 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/ColorParser.java @@ -0,0 +1,28 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import org.bukkit.Color; + +import java.util.Deque; + +public class ColorParser extends ParserType { + public ColorParser(Message message) { + super(message); + } + + @Override + public Color parse(Deque deque) throws CommandExecutionException { + String color = deque.pop(); + if (color.startsWith("0x")) color = color.substring(2); + if (color.startsWith("&")) color = color.substring(1); + if (color.startsWith("#")) color = color.substring(1); + try { + return Color.fromRGB(Integer.parseInt(color, 16)); + } catch (IllegalArgumentException exception) { + throw new CommandExecutionException(); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/EntityPropertyParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/EntityPropertyParser.java new file mode 100644 index 0000000..75dac21 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/EntityPropertyParser.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; + +import java.util.Deque; + +@SuppressWarnings("rawtypes") +public class EntityPropertyParser extends ParserType*/> { + private final EntityPropertyRegistryImpl propertyRegistry; + + public EntityPropertyParser(Message message, EntityPropertyRegistryImpl propertyRegistry) { + super(message); + this.propertyRegistry = propertyRegistry; + } + + @Override + public EntityPropertyImpl parse(Deque deque) throws CommandExecutionException { + EntityPropertyImpl property = propertyRegistry.getByName(deque.pop()); + if (property == null) throw new CommandExecutionException(); + return property; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/EnumParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/EnumParser.java new file mode 100644 index 0000000..344c1b9 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/EnumParser.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; + +import java.util.Deque; + +public class EnumParser> extends ParserType { + + private final Class enumClass; + + public EnumParser(Class enumClass, Message message) { + super(message); + this.enumClass = enumClass; + } + + @Override + public T parse(Deque deque) throws CommandExecutionException { + try { + return Enum.valueOf(enumClass, deque.pop().toUpperCase()); + } catch (IllegalArgumentException e) { + throw new CommandExecutionException(); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/InteractionTypeParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/InteractionTypeParser.java new file mode 100644 index 0000000..68adcaa --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/InteractionTypeParser.java @@ -0,0 +1,24 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.api.interaction.InteractionType; + +import java.util.Deque; + +public class InteractionTypeParser extends ParserType { + public InteractionTypeParser(Message message) { + super(message); + } + + @Override + public InteractionType parse(Deque deque) throws CommandExecutionException { + try { + return InteractionType.valueOf(deque.pop().toUpperCase()); + } catch (IllegalArgumentException ignored) { + throw new CommandExecutionException(); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NamedColorParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NamedColorParser.java new file mode 100644 index 0000000..33451a0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NamedColorParser.java @@ -0,0 +1,23 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.util.NamedColor; +import java.util.Deque; + +public class NamedColorParser extends ParserType { + public NamedColorParser(Message message) { + super(message); + } + + @Override + public NamedColor parse(Deque deque) throws CommandExecutionException { + try { + return NamedColor.valueOf(deque.pop()); + } catch (IllegalArgumentException exception) { + throw new CommandExecutionException(); + } + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NpcEntryParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NpcEntryParser.java new file mode 100644 index 0000000..bb452fa --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NpcEntryParser.java @@ -0,0 +1,26 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; + +import java.util.Deque; + +public class NpcEntryParser extends ParserType { + private final NpcRegistryImpl npcRegistry; + + public NpcEntryParser(NpcRegistryImpl npcRegistry, Message message) { + super(message); + this.npcRegistry = npcRegistry; + } + + @Override + public NpcEntryImpl parse(Deque deque) throws CommandExecutionException { + NpcEntryImpl entry = npcRegistry.getById(deque.pop()); + if (entry == null || !entry.isAllowCommandModification()) throw new CommandExecutionException(); + return entry; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NpcTypeParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NpcTypeParser.java new file mode 100644 index 0000000..c0c4f17 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NpcTypeParser.java @@ -0,0 +1,26 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.npc.NpcTypeImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; + +import java.util.Deque; + +public class NpcTypeParser extends ParserType { + private final NpcTypeRegistryImpl typeRegistry; + + public NpcTypeParser(Message message, NpcTypeRegistryImpl typeRegistry) { + super(message); + this.typeRegistry = typeRegistry; + } + + @Override + public NpcTypeImpl parse(Deque deque) throws CommandExecutionException { + NpcTypeImpl type = typeRegistry.getByName(deque.pop()); + if (type == null) throw new CommandExecutionException(); + return type; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/StringParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/StringParser.java new file mode 100644 index 0000000..c47ee6d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/StringParser.java @@ -0,0 +1,19 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; + +import java.util.Deque; + +public class StringParser extends ParserType { + public StringParser(Message message) { + super(message); + } + + @Override + public String parse(Deque deque) throws CommandExecutionException { + return String.join(" ", deque); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3fParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3fParser.java new file mode 100644 index 0000000..f2b9fb0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3fParser.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.util.Vector3f; + +import java.util.Deque; + +public class Vector3fParser extends ParserType { + public Vector3fParser(Message message) { + super(message); + } + + @Override + public Vector3f parse(Deque deque) throws CommandExecutionException { + try { + return new Vector3f( + Float.parseFloat(deque.pop()), + Float.parseFloat(deque.pop()), + Float.parseFloat(deque.pop())); + } catch (NumberFormatException e) { + throw new CommandExecutionException(); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3iParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3iParser.java new file mode 100644 index 0000000..286729d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3iParser.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.util.Vector3i; + +import java.util.Deque; + +public class Vector3iParser extends ParserType { + public Vector3iParser(Message message) { + super(message); + } + + @Override + public Vector3i parse(Deque deque) throws CommandExecutionException { + if (deque.size() == 0) { + return null; + } + try { + return new Vector3i( + Integer.parseInt(deque.pop()), + Integer.parseInt(deque.pop()), + Integer.parseInt(deque.pop())); + } catch (NumberFormatException e) { + throw new CommandExecutionException(); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionBuilder.java b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionBuilder.java new file mode 100644 index 0000000..c31b8ef --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionBuilder.java @@ -0,0 +1,126 @@ +package lol.pyr.znpcsplus.reflection; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.google.common.collect.ImmutableList; +import lol.pyr.znpcsplus.reflection.types.ClassReflection; +import lol.pyr.znpcsplus.reflection.types.FieldReflection; +import lol.pyr.znpcsplus.reflection.types.MethodReflection; + +import java.util.ArrayList; + +public class ReflectionBuilder { + private final String reflectionPackage; + private String fieldName; + private String additionalData; + private final ArrayList className = new ArrayList<>(); + private final ArrayList methods = new ArrayList<>(); + private final ArrayList[]> parameterTypes = new ArrayList<>(); + private Class expectType; + private boolean strict = true; + + public ReflectionBuilder() { + this(""); + } + + public ReflectionBuilder(Class clazz) { + this(""); + withClassName(clazz); + } + + public ReflectionBuilder(String reflectionPackage) { + this(reflectionPackage, "", "", null); + } + + protected ReflectionBuilder(String reflectionPackage, String fieldName, String additionalData, Class expectType) { + this.reflectionPackage = reflectionPackage; + this.fieldName = fieldName; + this.additionalData = additionalData; + this.expectType = expectType; + } + + public ReflectionBuilder withClassName(String className) { + this.className.add(ReflectionPackage.joinWithDot(reflectionPackage, PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_17) ? additionalData : "", className)); + return this; + } + + public ReflectionBuilder withRawClassName(String className) { + this.className.add(className); + return this; + } + + public ReflectionBuilder withClassName(Class clazz) { + if (clazz != null) className.add(clazz.getName()); + return this; + } + + public ReflectionBuilder withMethodName(String methodName) { + this.methods.add(methodName); + return this; + } + + public ReflectionBuilder withFieldName(String fieldName) { + this.fieldName = fieldName; + return this; + } + + public ReflectionBuilder withSubClass(String additionalData) { + this.additionalData = additionalData; + return this; + } + + public ReflectionBuilder withParameterTypes(Class... types) { + this.parameterTypes.add(types); + return this; + } + + public ReflectionBuilder withExpectResult(Class expectType) { + this.expectType = expectType; + return this; + } + + public ReflectionBuilder setStrict(boolean strict) { + this.strict = strict; + return this; + } + + public boolean isStrict() { + return strict; + } + + public Class getExpectType() { + return expectType; + } + + public ImmutableList[]> getParameterTypes() { + return ImmutableList.copyOf(this.parameterTypes); + } + + public ImmutableList getClassNames() { + return ImmutableList.copyOf(this.className); + } + + public ImmutableList getMethods() { + return ImmutableList.copyOf(this.methods); + } + + public String getPackage() { + return reflectionPackage; + } + + public String getFieldName() { + return fieldName; + } + + public MethodReflection toMethodReflection() { + return new MethodReflection(this); + } + + public ClassReflection toClassReflection() { + return new ClassReflection(this); + } + + public FieldReflection toFieldReflection() { + return new FieldReflection(this); + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionLazyLoader.java b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionLazyLoader.java new file mode 100644 index 0000000..01c4eaa --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionLazyLoader.java @@ -0,0 +1,55 @@ +package lol.pyr.znpcsplus.reflection; + +import com.github.retrooper.packetevents.PacketEvents; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.logging.Logger; + +public abstract class ReflectionLazyLoader { + private final static Logger logger = Logger.getLogger("ZNPCsPlus Reflection"); + protected final List possibleClassNames; + protected List> reflectionClasses = new ArrayList<>(); + protected final boolean strict; + private T cached; + private boolean loaded = false; + + protected ReflectionLazyLoader(ReflectionBuilder builder) { + this(builder.getClassNames(), builder.isStrict()); + } + + protected ReflectionLazyLoader(List possibleClassNames, boolean strict) { + this.possibleClassNames = possibleClassNames; + this.strict = strict; + for (String name : possibleClassNames) try { + reflectionClasses.add(Class.forName(name)); + } catch (ClassNotFoundException ignored) {} + } + + public T get() { + if (this.loaded) return this.cached; + try { + if (this.reflectionClasses.size() == 0) throw new ClassNotFoundException("No class found: " + possibleClassNames); + T eval = (this.cached != null) ? this.cached : (this.cached = load()); + if (eval == null) throw new RuntimeException("Returned value is null"); + } catch (Throwable throwable) { + if (strict) { + logger.warning(" ----- REFLECTION FAILURE DEBUG INFORMATION, REPORT THIS ON THE ZNPCSPLUS GITHUB ----- "); + logger.warning(getClass().getSimpleName() + " failed!"); + logger.warning("Class Names: " + possibleClassNames); + logger.warning("Reflection Type: " + getClass().getCanonicalName()); + logger.warning("Server Version: " + PacketEvents.getAPI().getServerManager().getVersion().name()); + printDebugInfo(logger::warning); + logger.warning("Exception:"); + throwable.printStackTrace(); + logger.warning(" ----- REFLECTION FAILURE DEBUG INFORMATION, REPORT THIS ON THE ZNPCSPLUS GITHUB ----- "); + } + } + this.loaded = true; + return this.cached; + } + + protected abstract T load() throws Exception; + protected void printDebugInfo(Consumer logger) {} +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionPackage.java b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionPackage.java new file mode 100644 index 0000000..4fb169f --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionPackage.java @@ -0,0 +1,40 @@ +package lol.pyr.znpcsplus.reflection; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import org.bukkit.Bukkit; + +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * A class containing getAll of the packages of the server jar that we import classes from. + * Every line has a check for the "flattened" variable due to the fact that server jars + * pre-1.17 had all of their classes "flattened" into one package. + */ +public class ReflectionPackage { + private static final String VERSION = generateVersion(); + public static final String BUKKIT = "org.bukkit.craftbukkit" + VERSION; + private static final boolean flattened = !PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_17); + + /** + * Check if the classes are flattened, if so we need to add the version string into the + * package string which is another quirk of the old server jars. + */ + public static final String MINECRAFT = joinWithDot("net.minecraft", flattened ? "server" + VERSION : ""); + public static final String ENTITY = flattened ? MINECRAFT : joinWithDot(MINECRAFT, "world.entity"); + + public static String joinWithDot(String... parts) { + return Arrays.stream(parts) + .filter(Objects::nonNull) + .filter(p -> !p.isEmpty()) + .collect(Collectors.joining(".")); + } + + private static String generateVersion() { + String[] parts = Bukkit.getServer().getClass().getPackage().getName().split("\\."); + if (parts.length > 3) return "." + parts[3]; + return ""; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/reflection/Reflections.java b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/Reflections.java new file mode 100644 index 0000000..b6e2dde --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/Reflections.java @@ -0,0 +1,253 @@ +package lol.pyr.znpcsplus.reflection; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import lol.pyr.znpcsplus.reflection.types.FieldReflection; +import lol.pyr.znpcsplus.util.FoliaUtil; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +/** + * Class containing getAll of the lazy-loaded reflections that the plugin + * uses to access inaccessible components of the server jar. + */ +public final class Reflections { + + /* + * Game profile methods used for obtaining raw skin data of online players + */ + + public static final Class GAME_PROFILE_CLASS = + new ReflectionBuilder() + .withRawClassName("com.mojang.authlib.GameProfile") + .toClassReflection().get(); + + public static final Class ENTITY_HUMAN_CLASS = + new ReflectionBuilder(ReflectionPackage.ENTITY) + .withSubClass("player") + .withClassName("EntityHuman") + .toClassReflection().get(); + + public static final ReflectionLazyLoader GET_PLAYER_HANDLE_METHOD = + new ReflectionBuilder(ReflectionPackage.BUKKIT) + .withClassName("entity.CraftPlayer") + .withClassName("entity.CraftHumanEntity") + .withMethodName("getHandle") + .withExpectResult(ENTITY_HUMAN_CLASS) + .toMethodReflection(); + + public static final ReflectionLazyLoader GET_PROFILE_METHOD = + new ReflectionBuilder(ReflectionPackage.ENTITY) + .withClassName(ENTITY_HUMAN_CLASS) + .withExpectResult(GAME_PROFILE_CLASS) + .toMethodReflection(); + + public static final Class PROPERTY_MAP_CLASS = + new ReflectionBuilder() + .withRawClassName("com.mojang.authlib.properties.PropertyMap") + .toClassReflection().get(); + + public static final ReflectionLazyLoader GET_PROPERTY_MAP_METHOD = + new ReflectionBuilder(GAME_PROFILE_CLASS) + .withMethodName("getProperties") + .withExpectResult(PROPERTY_MAP_CLASS) + .toMethodReflection(); + + public static final ReflectionLazyLoader PROPERTY_MAP_VALUES_METHOD = + new ReflectionBuilder() + .withClassName(PROPERTY_MAP_CLASS.getSuperclass()) + .withMethodName("values") + .withExpectResult(Collection.class) + .toMethodReflection(); + + public static final Class PROPERTY_CLASS = + new ReflectionBuilder() + .withRawClassName("com.mojang.authlib.properties.Property") + .toClassReflection().get(); + + private static final boolean v1_20_2 = PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_20_2); + + public static final ReflectionLazyLoader PROPERTY_GET_NAME_METHOD = + new ReflectionBuilder(PROPERTY_CLASS) + .withMethodName("getName") + .withExpectResult(String.class) + .toMethodReflection(); + + public static final ReflectionLazyLoader PROPERTY_NAME_FIELD = + new ReflectionBuilder(PROPERTY_CLASS) + .withFieldName("name") + .withExpectResult(String.class) + .setStrict(v1_20_2) + .toFieldReflection(); + + public static final ReflectionLazyLoader PROPERTY_GET_VALUE_METHOD = + new ReflectionBuilder(PROPERTY_CLASS) + .withMethodName("getValue") + .withExpectResult(String.class) + .toMethodReflection(); + + public static final ReflectionLazyLoader PROPERTY_VALUE_FIELD = + new ReflectionBuilder(PROPERTY_CLASS) + .withFieldName("value") + .withExpectResult(String.class) + .setStrict(v1_20_2) + .toFieldReflection(); + + public static final ReflectionLazyLoader PROPERTY_GET_SIGNATURE_METHOD = + new ReflectionBuilder(PROPERTY_CLASS) + .withMethodName("getSignature") + .withExpectResult(String.class) + .toMethodReflection(); + + public static final ReflectionLazyLoader PROPERTY_SIGNATURE_FIELD = + new ReflectionBuilder(PROPERTY_CLASS) + .withFieldName("signature") + .withExpectResult(String.class) + .setStrict(v1_20_2) + .toFieldReflection(); + + /* + * These methods are used for reserving entity ids so regular Minecraft + * entity packets don't interfere with our packet-based entities + */ + + public static final Class ENTITY_CLASS = + new ReflectionBuilder(ReflectionPackage.ENTITY) + .withClassName("Entity") + .toClassReflection().get(); + public static final FieldReflection.ValueModifier ENTITY_ID_MODIFIER = + new ReflectionBuilder(ReflectionPackage.ENTITY) + .withClassName(ENTITY_CLASS) + .withFieldName("entityCount") + .setStrict(!PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_14)) + .toFieldReflection() + .toStaticValueModifier(int.class); + + public static final ReflectionLazyLoader ATOMIC_ENTITY_ID_FIELD = + new ReflectionBuilder(ReflectionPackage.ENTITY) + .withClassName(ENTITY_CLASS) + .withFieldName("entityCount") + .withFieldName("d") + .withFieldName("c") + .withExpectResult(AtomicInteger.class) + .setStrict(PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_14)) + .toFieldReflection() + .toStaticValueLoader(AtomicInteger.class); + + /* + * All of these folia methods need to be reflected because folia is strictly + * available on the newest java versions but we need to keep support for Java 8 + */ + + public static final Class ASYNC_SCHEDULER_CLASS = + new ReflectionBuilder("io.papermc.paper.threadedregions.scheduler") + .withClassName("AsyncScheduler") + .setStrict(FoliaUtil.isFolia()) + .toClassReflection().get(); + + public static final Class GLOBAL_REGION_SCHEDULER_CLASS = + new ReflectionBuilder("io.papermc.paper.threadedregions.scheduler") + .withClassName("GlobalRegionScheduler") + .setStrict(FoliaUtil.isFolia()) + .toClassReflection().get(); + + public static final Class REGION_SCHEDULER_CLASS = + new ReflectionBuilder("io.papermc.paper.threadedregions.scheduler") + .withClassName("RegionScheduler") + .setStrict(FoliaUtil.isFolia()) + .toClassReflection().get(); + + public static final Class SCHEDULED_TASK_CLASS = + new ReflectionBuilder("io.papermc.paper.threadedregions.scheduler") + .withClassName("ScheduledTask") + .setStrict(FoliaUtil.isFolia()) + .toClassReflection().get(); + + public static final ReflectionLazyLoader FOLIA_GET_ASYNC_SCHEDULER = + new ReflectionBuilder(Bukkit.class) + .withMethodName("getAsyncScheduler") + .withExpectResult(ASYNC_SCHEDULER_CLASS) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_GET_GLOBAL_REGION_SCHEDULER = + new ReflectionBuilder(Bukkit.class) + .withMethodName("getGlobalRegionScheduler") + .withExpectResult(GLOBAL_REGION_SCHEDULER_CLASS) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_GET_REGION_SCHEDULER = + new ReflectionBuilder(Bukkit.class) + .withMethodName("getRegionScheduler") + .withExpectResult(REGION_SCHEDULER_CLASS) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_RUN_NOW_ASYNC = + new ReflectionBuilder(ASYNC_SCHEDULER_CLASS) + .withMethodName("runNow") + .withParameterTypes(Plugin.class, Consumer.class) + .withExpectResult(SCHEDULED_TASK_CLASS) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_RUN_DELAYED_ASYNC = + new ReflectionBuilder(ASYNC_SCHEDULER_CLASS) + .withMethodName("runDelayed") + .withParameterTypes(Plugin.class, Consumer.class, long.class, TimeUnit.class) + .withExpectResult(SCHEDULED_TASK_CLASS) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_RUN_AT_FIXED_RATE_ASYNC = + new ReflectionBuilder(ASYNC_SCHEDULER_CLASS) + .withMethodName("runAtFixedRate") + .withParameterTypes(Plugin.class, Consumer.class, long.class, long.class, TimeUnit.class) + .withExpectResult(SCHEDULED_TASK_CLASS) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_RUN_NOW_GLOBAL = + new ReflectionBuilder(GLOBAL_REGION_SCHEDULER_CLASS) + .withMethodName("runNow") + .withParameterTypes(Plugin.class, Consumer.class) + .withExpectResult(SCHEDULED_TASK_CLASS) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_EXECUTE_REGION = + new ReflectionBuilder(REGION_SCHEDULER_CLASS) + .withMethodName("execute") + .withParameterTypes(Plugin.class, Location.class, Runnable.class) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_CANCEL_ASYNC_TASKS = + new ReflectionBuilder(ASYNC_SCHEDULER_CLASS) + .withMethodName("cancelTasks") + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_CANCEL_GLOBAL_TASKS = + new ReflectionBuilder(GLOBAL_REGION_SCHEDULER_CLASS) + .withMethodName("cancelTasks") + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_TELEPORT_ASYNC = + new ReflectionBuilder(Entity.class) + .withMethodName("teleportAsync") + .withParameterTypes(Location.class) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/ClassReflection.java b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/ClassReflection.java new file mode 100644 index 0000000..de3f106 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/ClassReflection.java @@ -0,0 +1,14 @@ +package lol.pyr.znpcsplus.reflection.types; + +import lol.pyr.znpcsplus.reflection.ReflectionBuilder; +import lol.pyr.znpcsplus.reflection.ReflectionLazyLoader; + +public class ClassReflection extends ReflectionLazyLoader> { + public ClassReflection(ReflectionBuilder reflectionBuilder) { + super(reflectionBuilder); + } + + protected Class load() { + return this.reflectionClasses.size() > 0 ? this.reflectionClasses.get(0) : null; + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/FieldReflection.java b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/FieldReflection.java new file mode 100644 index 0000000..67c39fe --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/FieldReflection.java @@ -0,0 +1,123 @@ +package lol.pyr.znpcsplus.reflection.types; + +import lol.pyr.znpcsplus.reflection.ReflectionLazyLoader; +import lol.pyr.znpcsplus.reflection.ReflectionBuilder; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.function.Consumer; + +public class FieldReflection extends ReflectionLazyLoader { + private final String fieldName; + private final Class expectType; + + public FieldReflection(ReflectionBuilder builder) { + super(builder); + this.fieldName = builder.getFieldName(); + this.expectType = builder.getExpectType(); + } + + protected Field load() { + if (fieldName != null && fieldName.length() > 0) for (Class clazz : this.reflectionClasses) { + Field field = loadByName(clazz); + if (field != null) return field; + } + if (expectType != null) for (Class clazz : this.reflectionClasses) { + Field field = loadByType(clazz); + if (field != null) return field; + } + return null; + } + + private Field loadByName(Class clazz) { + try { + Field field = clazz.getDeclaredField(fieldName); + if (expectType != null && !field.getType().equals(expectType)) return null; + field.setAccessible(true); + return field; + } catch (NoSuchFieldException ignored) {} + return null; + } + + private Field loadByType(Class clazz) { + for (Field field : clazz.getDeclaredFields()) if (field.getType() == expectType) { + field.setAccessible(true); + return field; + } + return null; + } + + @Override + protected void printDebugInfo(Consumer logger) { + logger.accept("Field Name: " + fieldName); + logger.accept("Field Type: " + expectType); + } + + public ValueReflection toStaticValueLoader() { + return toStaticValueLoader(Object.class); + } + + @SuppressWarnings("unused") + public ValueReflection toStaticValueLoader(Class valueType) { + return new ValueReflection<>(this, possibleClassNames, null, strict); + } + + @SuppressWarnings("unused") + public ValueReflection toValueLoader(Object obj, Class valueType) { + return new ValueReflection<>(this, possibleClassNames, obj, strict); + } + + @SuppressWarnings("unused") + public ValueModifier toStaticValueModifier(Class valueType) { + return new ValueModifier<>(this, possibleClassNames, null, strict); + } + + @SuppressWarnings("unused") + public ValueModifier toValueModifier(Object obj, Class valueType) { + return new ValueModifier<>(this, possibleClassNames, obj, strict); + } + + public static class ValueReflection extends ReflectionLazyLoader { + protected final Object obj; + protected final FieldReflection fieldReflection; + + private ValueReflection(FieldReflection fieldReflection, List className, Object obj, boolean strict) { + super(className, strict); + this.obj = obj; + this.fieldReflection = fieldReflection; + } + + @SuppressWarnings("unchecked") + protected T load() throws IllegalAccessException, NoSuchFieldException, ClassCastException { + return (T) this.fieldReflection.get().get(obj); + } + + @Override + protected void printDebugInfo(Consumer logger) { + fieldReflection.printDebugInfo(logger); + } + } + + public static class ValueModifier extends ValueReflection { + private ValueModifier(FieldReflection fieldReflection, List className, Object obj, boolean strict) { + super(fieldReflection, className, obj, strict); + } + + @Override + public T get() { + try { + return load(); + } catch (IllegalAccessException | NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + public void set(T value) { + try { + fieldReflection.get().set(obj, value); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/MethodReflection.java b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/MethodReflection.java new file mode 100644 index 0000000..c758727 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/MethodReflection.java @@ -0,0 +1,63 @@ +package lol.pyr.znpcsplus.reflection.types; + +import com.google.common.collect.ImmutableList; +import lol.pyr.znpcsplus.reflection.ReflectionBuilder; +import lol.pyr.znpcsplus.reflection.ReflectionLazyLoader; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.function.Consumer; + +public class MethodReflection extends ReflectionLazyLoader { + private final ImmutableList methods; + private final ImmutableList[]> parameterTypes; + private final Class expectType; + + public MethodReflection(ReflectionBuilder builder) { + super(builder); + this.methods = builder.getMethods(); + this.expectType = builder.getExpectType(); + this.parameterTypes = builder.getParameterTypes(); + } + + protected Method load() { + Map> imperfectMatches = new HashMap<>(); + for (Class clazz : this.reflectionClasses) { + Method method = load(clazz, imperfectMatches); + if (method != null) return method; + } + for (int i = 2; i > 0; i--) if (imperfectMatches.containsKey(i)) { + return imperfectMatches.get(i).get(0); + } + return null; + } + + private Method load(Class clazz, Map> imperfectMatches) { + for (Method method : clazz.getDeclaredMethods()) { + int matches = 0; + if (expectType != null) { + if (!method.getReturnType().equals(expectType)) continue; + matches++; + } + if (parameterTypes.size() > 0) out: for (Class[] possible : parameterTypes) { + if (method.getParameterCount() != possible.length) continue; + for (int i = 0; i < possible.length; i++) if (!method.getParameterTypes()[i].equals(possible[i])) continue out; + matches++; + } + if (methods.contains(method.getName())) { + matches++; + } + if (matches == 3) return method; + else imperfectMatches.computeIfAbsent(matches, i -> new ArrayList<>()).add(method); + } + return null; + } + + @Override + protected void printDebugInfo(Consumer logger) { + logger.accept("Expected Return Type: " + expectType); + logger.accept("Possible method names: " + methods); + logger.accept("Possible Parameter Type Combinations:"); + for (Class[] possible : parameterTypes) logger.accept(Arrays.toString(possible)); + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/FoliaScheduler.java b/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/FoliaScheduler.java new file mode 100644 index 0000000..0436b39 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/FoliaScheduler.java @@ -0,0 +1,92 @@ +package lol.pyr.znpcsplus.scheduling; + +import lol.pyr.znpcsplus.reflection.Reflections; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public class FoliaScheduler extends TaskScheduler { + public FoliaScheduler(Plugin plugin) { + super(plugin); + } + + @Override + public void schedulePlayerChat(Player player, String chat) { + try { + Object scheduler = Reflections.FOLIA_GET_REGION_SCHEDULER.get().invoke(null); + Reflections.FOLIA_EXECUTE_REGION.get().invoke(scheduler, plugin, player.getLocation(), (Runnable) () -> player.chat(chat)); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Override + public void schedulePlayerCommand(Player player, String command) { + try { + Object scheduler = Reflections.FOLIA_GET_REGION_SCHEDULER.get().invoke(null); + Reflections.FOLIA_EXECUTE_REGION.get().invoke(scheduler, plugin, player.getLocation(), (Runnable) () -> Bukkit.dispatchCommand(player, command)); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public void runSyncGlobal(Runnable runnable) { + try { + Object scheduler = Reflections.FOLIA_GET_GLOBAL_REGION_SCHEDULER.get().invoke(null); + Reflections.FOLIA_RUN_NOW_GLOBAL.get().invoke(scheduler, plugin, (Consumer) o -> runnable.run()); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public void runAsyncGlobal(Runnable runnable) { + try { + Object scheduler = Reflections.FOLIA_GET_ASYNC_SCHEDULER.get().invoke(null); + Reflections.FOLIA_RUN_NOW_ASYNC.get().invoke(scheduler, plugin, (Consumer) o -> runnable.run()); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public void runLaterAsync(Runnable runnable, long delay) { + try { + Object scheduler = Reflections.FOLIA_GET_ASYNC_SCHEDULER.get().invoke(null); + Reflections.FOLIA_RUN_DELAYED_ASYNC.get().invoke(scheduler, plugin, (Consumer) o -> runnable.run(), delay * 50, TimeUnit.MILLISECONDS); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public void runDelayedTimerAsync(Runnable runnable, long delay, long interval) { + try { + Object scheduler = Reflections.FOLIA_GET_ASYNC_SCHEDULER.get().invoke(null); + Reflections.FOLIA_RUN_AT_FIXED_RATE_ASYNC.get().invoke(scheduler, plugin, (Consumer) o -> runnable.run(), delay * 50, interval * 50, TimeUnit.MILLISECONDS); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public void cancelAll() { + try { + Object asyncScheduler = Reflections.FOLIA_GET_ASYNC_SCHEDULER.get().invoke(null); + Reflections.FOLIA_CANCEL_ASYNC_TASKS.get().invoke(asyncScheduler, plugin); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + try { + Object globalScheduler = Reflections.FOLIA_GET_GLOBAL_REGION_SCHEDULER.get().invoke(null); + Reflections.FOLIA_CANCEL_GLOBAL_TASKS.get().invoke(globalScheduler, plugin); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } +} + diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/SpigotScheduler.java b/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/SpigotScheduler.java new file mode 100644 index 0000000..949218a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/SpigotScheduler.java @@ -0,0 +1,46 @@ +package lol.pyr.znpcsplus.scheduling; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +public class SpigotScheduler extends TaskScheduler { + public SpigotScheduler(Plugin plugin) { + super(plugin); + } + + @Override + public void schedulePlayerChat(Player player, String chat) { + runSyncGlobal(() -> player.chat(chat)); + } + + @Override + public void schedulePlayerCommand(Player player, String command) { + runSyncGlobal(() -> Bukkit.dispatchCommand(player, command)); + } + + @Override + public void runSyncGlobal(Runnable runnable) { + Bukkit.getScheduler().runTask(plugin, runnable); + } + + @Override + public void runAsyncGlobal(Runnable runnable) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, runnable); + } + + @Override + public void runLaterAsync(Runnable runnable, long delay) { + Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, runnable, delay); + } + + @Override + public void runDelayedTimerAsync(Runnable runnable, long delay, long interval) { + Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, runnable, delay, interval); + } + + @Override + public void cancelAll() { + Bukkit.getScheduler().cancelTasks(plugin); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/TaskScheduler.java b/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/TaskScheduler.java new file mode 100644 index 0000000..fca0bf0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/TaskScheduler.java @@ -0,0 +1,20 @@ +package lol.pyr.znpcsplus.scheduling; + +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +public abstract class TaskScheduler { + protected final Plugin plugin; + + public TaskScheduler(Plugin plugin) { + this.plugin = plugin; + } + + public abstract void schedulePlayerChat(Player player, String message); + public abstract void schedulePlayerCommand(Player player, String command); + public abstract void runSyncGlobal(Runnable runnable); + public abstract void runAsyncGlobal(Runnable runnable); + public abstract void runLaterAsync(Runnable runnable, long delay); + public abstract void runDelayedTimerAsync(Runnable runnable, long delay, long interval); + public abstract void cancelAll(); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/serialization/NpcSerializerRegistryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/serialization/NpcSerializerRegistryImpl.java new file mode 100644 index 0000000..71a7a95 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/serialization/NpcSerializerRegistryImpl.java @@ -0,0 +1,33 @@ +package lol.pyr.znpcsplus.serialization; + +import lol.pyr.znpcsplus.api.serialization.NpcSerializer; +import lol.pyr.znpcsplus.api.serialization.NpcSerializerRegistry; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.util.HashMap; +import java.util.Map; + +public class NpcSerializerRegistryImpl implements NpcSerializerRegistry { + private final Map, NpcSerializer> serializerMap = new HashMap<>(); + + public NpcSerializerRegistryImpl(PacketFactory packetFactory, ConfigManager configManager, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer) { + registerSerializer(YamlConfiguration.class, new YamlSerializer(packetFactory, configManager, actionRegistry, typeRegistry, propertyRegistry, textSerializer)); + } + + @SuppressWarnings("unchecked") + @Override + public NpcSerializer getSerializer(Class clazz) { + return (NpcSerializer) serializerMap.get(clazz); + } + + @Override + public void registerSerializer(Class clazz, NpcSerializer serializer) { + serializerMap.put(clazz, serializer); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/serialization/YamlSerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/serialization/YamlSerializer.java new file mode 100644 index 0000000..094a493 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/serialization/YamlSerializer.java @@ -0,0 +1,154 @@ +package lol.pyr.znpcsplus.serialization; + +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.npc.NpcEntry; +import lol.pyr.znpcsplus.api.serialization.NpcSerializer; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class YamlSerializer implements NpcSerializer { + private final static Logger logger = Logger.getLogger("YamlSerializer"); + + private final PacketFactory packetFactory; + private final ConfigManager configManager; + private final ActionRegistryImpl actionRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final LegacyComponentSerializer textSerializer; + + public YamlSerializer(PacketFactory packetFactory, ConfigManager configManager, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer) { + this.packetFactory = packetFactory; + this.configManager = configManager; + this.actionRegistry = actionRegistry; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.textSerializer = textSerializer; + } + + @Override + public YamlConfiguration serialize(NpcEntry entry) { + YamlConfiguration config = new YamlConfiguration(); + config.set("id", entry.getId()); + config.set("is-processed", entry.isProcessed()); + config.set("allow-commands", entry.isAllowCommandModification()); + config.set("save", entry.isSave()); + + NpcImpl npc = (NpcImpl) entry.getNpc(); + config.set("enabled", npc.isEnabled()); + config.set("uuid", npc.getUuid().toString()); + config.set("world", npc.getWorldName()); + config.set("location", serializeLocation(npc.getLocation())); + config.set("type", npc.getType().getName()); + + for (EntityProperty property : npc.getAllProperties()) try { + PropertySerializer serializer = propertyRegistry.getSerializer(((EntityPropertyImpl) property).getType()); + if (serializer == null) { + Bukkit.getLogger().log(Level.WARNING, "Unknown serializer for property '" + property.getName() + "' for npc '" + entry.getId() + "'. skipping ..."); + continue; + } + config.set("properties." + property.getName(), serializer.UNSAFE_serialize(npc.getProperty(property))); + } catch (Exception exception) { + logger.severe("Failed to serialize property " + property.getName() + " for npc with id " + entry.getId()); + exception.printStackTrace(); + } + + HologramImpl hologram = npc.getHologram(); + if (hologram.getOffset() != 0.0) config.set("hologram.offset", hologram.getOffset()); + if (hologram.getRefreshDelay() != -1) config.set("hologram.refresh-delay", hologram.getRefreshDelay()); + List lines = new ArrayList<>(npc.getHologram().getLines().size()); + for (int i = 0; i < hologram.getLines().size(); i++) { + lines.add(hologram.getLine(i)); + } + config.set("hologram.lines", lines); + config.set("actions", npc.getActions().stream() + .map(actionRegistry::serialize) + .filter(Objects::nonNull) + .collect(Collectors.toList())); + + return config; + } + + @Override + public NpcEntry deserialize(YamlConfiguration config) { + UUID uuid = config.contains("uuid") ? UUID.fromString(config.getString("uuid")) : UUID.randomUUID(); + NpcImpl npc = new NpcImpl(uuid, propertyRegistry, configManager, packetFactory, textSerializer, config.getString("world"), + typeRegistry.getByName(config.getString("type")), deserializeLocation(config.getConfigurationSection("location"))); + + if (config.isBoolean("enabled")) npc.setEnabled(config.getBoolean("enabled")); + + ConfigurationSection properties = config.getConfigurationSection("properties"); + if (properties != null) { + for (String key : properties.getKeys(false)) { + EntityPropertyImpl property = propertyRegistry.getByName(key); + if (property == null) { + Bukkit.getLogger().log(Level.WARNING, "Unknown property '" + key + "' for npc '" + config.getString("id") + "'. skipping ..."); + continue; + } + PropertySerializer serializer = propertyRegistry.getSerializer(property.getType()); + if (serializer == null) { + Bukkit.getLogger().log(Level.WARNING, "Unknown serializer for property '" + key + "' for npc '" + config.getString("id") + "'. skipping ..."); + continue; + } + Object value = serializer.deserialize(properties.getString(key)); + if (value == null) { + Bukkit.getLogger().log(Level.WARNING, "Failed to deserialize property '" + key + "' for npc '" + config.getString("id") + "'. Resetting to default ..."); + value = property.getDefaultValue(); + } + npc.UNSAFE_setProperty(property, value); + } + } + HologramImpl hologram = npc.getHologram(); + hologram.setOffset(config.getDouble("hologram.offset", 0.0)); + hologram.setRefreshDelay(config.getLong("hologram.refresh-delay", -1)); + for (String line : config.getStringList("hologram.lines")) hologram.addLine(line); + for (String s : config.getStringList("actions")) npc.addAction(actionRegistry.deserialize(s)); + + NpcEntryImpl entry = new NpcEntryImpl(config.getString("id"), npc); + entry.setProcessed(config.getBoolean("is-processed")); + entry.setAllowCommandModification(config.getBoolean("allow-commands")); + entry.setSave(config.getBoolean("save", true)); + + return entry; + } + + public NpcLocation deserializeLocation(ConfigurationSection section) { + return new NpcLocation( + section.getDouble("x"), + section.getDouble("y"), + section.getDouble("z"), + (float) section.getDouble("yaw"), + (float) section.getDouble("pitch") + ); + } + + public YamlConfiguration serializeLocation(NpcLocation location) { + YamlConfiguration config = new YamlConfiguration(); + config.set("x", location.getX()); + config.set("y", location.getY()); + config.set("z", location.getZ()); + config.set("yaw", location.getYaw()); + config.set("pitch", location.getPitch()); + return config; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/BaseSkinDescriptor.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/BaseSkinDescriptor.java new file mode 100644 index 0000000..1fe1623 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/BaseSkinDescriptor.java @@ -0,0 +1,44 @@ +package lol.pyr.znpcsplus.skin; + +import com.github.retrooper.packetevents.protocol.player.TextureProperty; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.skin.descriptor.NameFetchingDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.MirrorDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.UUIDFetchingDescriptor; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public interface BaseSkinDescriptor extends SkinDescriptor { + CompletableFuture fetch(Player player); + SkinImpl fetchInstant(Player player); + boolean supportsInstant(Player player); + String serialize(); + + static BaseSkinDescriptor deserialize(MojangSkinCache skinCache, String str) { + String[] arr = str.split(";"); + if (arr[0].equalsIgnoreCase("mirror")) return new MirrorDescriptor(skinCache); + else if (arr[0].equalsIgnoreCase("fetching-uuid")) { + String value = String.join(";", Arrays.copyOfRange(arr, 1, arr.length)); + return new UUIDFetchingDescriptor(skinCache, UUID.fromString(value)); + } + else if(arr[0].equalsIgnoreCase("fetching")) { + String value = String.join(";", Arrays.copyOfRange(arr, 1, arr.length)); + return new NameFetchingDescriptor(skinCache, value); + } + else if (arr[0].equalsIgnoreCase("prefetched")) { + List properties = new ArrayList<>(); + for (int i = 0; i < (arr.length - 1) / 3; i++) { + properties.add(new TextureProperty(arr[i + 1], arr[i + 2], arr[i + 3])); + } + return new PrefetchedDescriptor(new SkinImpl(properties)); + } + throw new IllegalArgumentException("Unknown SkinDescriptor type!"); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinDescriptorFactoryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinDescriptorFactoryImpl.java new file mode 100644 index 0000000..10a8cea --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinDescriptorFactoryImpl.java @@ -0,0 +1,67 @@ +package lol.pyr.znpcsplus.skin; + +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.api.skin.SkinDescriptorFactory; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.skin.descriptor.NameFetchingDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.MirrorDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.UUIDFetchingDescriptor; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.UUID; + +public class SkinDescriptorFactoryImpl implements SkinDescriptorFactory { + private final MojangSkinCache skinCache; + private final MirrorDescriptor mirrorDescriptor; + + public SkinDescriptorFactoryImpl(MojangSkinCache skinCache) { + this.skinCache = skinCache; + mirrorDescriptor = new MirrorDescriptor(skinCache); + } + + @Override + public SkinDescriptor createMirrorDescriptor() { + return mirrorDescriptor; + } + + @Override + public SkinDescriptor createRefreshingDescriptor(String playerName) { + return new NameFetchingDescriptor(skinCache, playerName); + } + + @Override + public SkinDescriptor createRefreshingDescriptor(UUID playerUUID) { + return new UUIDFetchingDescriptor(skinCache, playerUUID); + } + + @Override + public SkinDescriptor createStaticDescriptor(String playerName) { + return PrefetchedDescriptor.forPlayer(skinCache, playerName).join(); + } + + @Override + public SkinDescriptor createStaticDescriptor(String texture, String signature) { + return new PrefetchedDescriptor(new SkinImpl(texture, signature)); + } + + @Override + public SkinDescriptor createUrlDescriptor(String url, String variant) { + try { + return createUrlDescriptor(new URL(url), variant); + } catch (MalformedURLException e) { + return null; + } + } + + @Override + public SkinDescriptor createUrlDescriptor(URL url, String variant) { + return PrefetchedDescriptor.fromUrl(skinCache, url, variant).join(); + } + + @Override + public SkinDescriptor createFileDescriptor(String path) { + return PrefetchedDescriptor.fromFile(skinCache, path).join(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinImpl.java new file mode 100644 index 0000000..66bc999 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinImpl.java @@ -0,0 +1,91 @@ +package lol.pyr.znpcsplus.skin; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.protocol.player.TextureProperty; +import com.github.retrooper.packetevents.protocol.player.UserProfile; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lol.pyr.znpcsplus.api.skin.Skin; +import lol.pyr.znpcsplus.reflection.Reflections; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class SkinImpl implements Skin { + private final long timestamp = System.currentTimeMillis(); + private final List properties; + private static final boolean V1_20_2 = PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_20_2); + + public SkinImpl(String texture, String signature) { + properties = new ArrayList<>(1); + properties.add(new TextureProperty("textures", texture, signature)); + } + + public SkinImpl(Collection properties) { + this.properties = new ArrayList<>(properties); + } + + public SkinImpl(Object propertyMap) { + this.properties = new ArrayList<>(); + try { + Collection properties = (Collection) Reflections.PROPERTY_MAP_VALUES_METHOD.get().invoke(propertyMap); + for (Object property : properties) { + String name; + String value; + String signature; + if (V1_20_2) { + name = (String) Reflections.PROPERTY_NAME_FIELD.get().get(property); + value = (String) Reflections.PROPERTY_VALUE_FIELD.get().get(property); + signature = (String) Reflections.PROPERTY_SIGNATURE_FIELD.get().get(property); + } else { + name = (String) Reflections.PROPERTY_GET_NAME_METHOD.get().invoke(property); + value = (String) Reflections.PROPERTY_GET_VALUE_METHOD.get().invoke(property); + signature = (String) Reflections.PROPERTY_GET_SIGNATURE_METHOD.get().invoke(property); + } + this.properties.add(new TextureProperty(name, value, signature)); + } + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + public SkinImpl(JsonObject obj) { + properties = new ArrayList<>(); + for (JsonElement e : obj.get("properties").getAsJsonArray()) { + JsonObject o = e.getAsJsonObject(); + properties.add(new TextureProperty(o.get("name").getAsString(), o.get("value").getAsString(), o.has("signature") ? o.get("signature").getAsString() : null)); + } + } + + public UserProfile apply(UserProfile profile) { + profile.setTextureProperties(properties); + return profile; + } + + public List getProperties() { + return properties; + } + + public boolean isExpired() { + return System.currentTimeMillis() - timestamp > 60000L; + } + + @Override + public String getTexture() { + for (TextureProperty property : properties) + if (property.getName().equalsIgnoreCase("textures")) + return property.getValue(); + return null; + } + + @Override + public String getSignature() { + for (TextureProperty property : properties) + if (property.getName().equalsIgnoreCase("textures")) + return property.getSignature(); + return null; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/CachedId.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/CachedId.java new file mode 100644 index 0000000..cf60441 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/CachedId.java @@ -0,0 +1,18 @@ +package lol.pyr.znpcsplus.skin.cache; + +public class CachedId { + private final long timestamp = System.currentTimeMillis(); + private final String id; + + public CachedId(String id) { + this.id = id; + } + + public boolean isExpired() { + return System.currentTimeMillis() - timestamp > 60000L; + } + + public String getId() { + return id; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/MojangSkinCache.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/MojangSkinCache.java new file mode 100644 index 0000000..3fe0fd3 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/MojangSkinCache.java @@ -0,0 +1,277 @@ +package lol.pyr.znpcsplus.skin.cache; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.reflection.Reflections; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.util.FutureUtil; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +public class MojangSkinCache { + private final static Logger logger = Logger.getLogger("ZNPCsPlus Skin Cache"); + + private final ConfigManager configManager; + + private final Map cache = new ConcurrentHashMap<>(); + private final Map idCache = new ConcurrentHashMap<>(); + private final File skinsFolder; + + public MojangSkinCache(ConfigManager configManager, File skinsFolder) { + this.configManager = configManager; + this.skinsFolder = skinsFolder; + if (!skinsFolder.exists()) skinsFolder.mkdirs(); + } + + public void cleanCache() { + for (Map.Entry entry : cache.entrySet()) if (entry.getValue().isExpired()) cache.remove(entry.getKey()); + for (Map.Entry entry : idCache.entrySet()) if (entry.getValue().isExpired()) cache.remove(entry.getKey()); + } + + public CompletableFuture fetchByName(String name) { + Player player = Bukkit.getPlayerExact(name); + if (player != null && player.isOnline()) return CompletableFuture.completedFuture(getFromPlayer(player)); + + if (idCache.containsKey(name.toLowerCase())) return fetchByUUID(idCache.get(name.toLowerCase()).getId()); + + return FutureUtil.exceptionPrintingSupplyAsync(() -> { + URL url = parseUrl("https://api.minecraftservices.com/minecraft/profile/lookup/name/" + name); + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + try (Reader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { + JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject(); + if (obj.has("errorMessage")) return fetchByNameFallback(name).join(); + String id = obj.get("id").getAsString(); + idCache.put(name.toLowerCase(), new CachedId(id)); + SkinImpl skin = fetchByUUID(id).join(); + if (skin == null) return fetchByNameFallback(name).join(); + return skin; + } + } catch (IOException exception) { + if (!configManager.getConfig().disableSkinFetcherWarnings()) { + logger.warning("Failed to get uuid from player name, trying to use fallback server:"); + exception.printStackTrace(); + } + return fetchByNameFallback(name).join(); + } finally { + if (connection != null) connection.disconnect(); + } + }); + } + + public CompletableFuture fetchByNameFallback(String name) { + Player player = Bukkit.getPlayerExact(name); + if (player != null && player.isOnline()) return CompletableFuture.completedFuture(getFromPlayer(player)); + + if (idCache.containsKey(name.toLowerCase())) return fetchByUUID(idCache.get(name.toLowerCase()).getId()); + + return FutureUtil.exceptionPrintingSupplyAsync(() -> { + URL url = parseUrl("https://api.ashcon.app/mojang/v2/user/" + name); + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + try (Reader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { + JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject(); + if (obj.has("error")) return null; + String uuid = obj.get("uuid").getAsString(); + idCache.put(name.toLowerCase(), new CachedId(uuid)); + JsonObject textures = obj.get("textures").getAsJsonObject(); + String value = textures.get("raw").getAsJsonObject().get("value").getAsString(); + String signature = textures.get("raw").getAsJsonObject().get("signature").getAsString(); + SkinImpl skin = new SkinImpl(value, signature); + cache.put(uuid, skin); + return skin; + } + } catch (IOException exception) { + if (!configManager.getConfig().disableSkinFetcherWarnings()) { + logger.warning("Failed to fetch skin from fallback server:"); + exception.printStackTrace(); + } + } finally { + if (connection != null) connection.disconnect(); + } + return null; + }); + } + + public CompletableFuture fetchByUrl(URL url, String variant) { + return FutureUtil.exceptionPrintingSupplyAsync(() -> { + URL apiUrl = parseUrl("https://api.mineskin.org/generate/url"); + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) apiUrl.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("accept", "application/json"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + OutputStream outStream = connection.getOutputStream(); + DataOutputStream out = new DataOutputStream(outStream); + out.writeBytes("{\"variant\":\"" + variant + "\",\"url\":\"" + url.toString() + "\"}"); + out.flush(); + out.close(); + outStream.close(); + + try (Reader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { + JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject(); + if (obj.has("error")) return null; + if (!obj.has("data")) return null; + JsonObject texture = obj.get("data").getAsJsonObject().get("texture").getAsJsonObject(); + return new SkinImpl(texture.get("value").getAsString(), texture.get("signature").getAsString()); + } + + } catch (IOException exception) { + if (!configManager.getConfig().disableSkinFetcherWarnings()) { + logger.warning("Failed to get skin from url:"); + exception.printStackTrace(); + } + } finally { + if (connection != null) connection.disconnect(); + } + return null; + }); + } + + public CompletableFuture fetchFromFile(String path) throws FileNotFoundException { + File file = new File(skinsFolder, path); + if (!file.exists()) throw new FileNotFoundException("File not found: " + path); + return CompletableFuture.supplyAsync(() -> { + URL apiUrl = parseUrl("https://api.mineskin.org/generate/upload"); + HttpURLConnection connection = null; + try { + String boundary = "*****"; + String CRLF = "\r\n"; + + connection = (HttpURLConnection) apiUrl.openConnection(); + connection.setRequestMethod("POST"); + connection.setReadTimeout(10000); + connection.setConnectTimeout(15000); + connection.setUseCaches(false); + connection.setRequestProperty("Cache-Control", "no-cache"); + connection.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary); + connection.setDoInput(true); + connection.setDoOutput(true); + OutputStream outputStream = connection.getOutputStream(); + DataOutputStream out = new DataOutputStream(outputStream); + out.writeBytes("--" + boundary + CRLF); + out.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"" + file.getName() + "\"" + CRLF); + out.writeBytes("Content-Type: image/png" + CRLF); + out.writeBytes(CRLF); + out.write(Files.readAllBytes(file.toPath())); + out.writeBytes(CRLF); + out.writeBytes("--" + boundary + "--" + CRLF); + out.flush(); + out.close(); + outputStream.close(); + + try (Reader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { + JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject(); + if (obj.has("error")) return null; + if (!obj.has("data")) return null; + JsonObject texture = obj.get("data").getAsJsonObject().get("texture").getAsJsonObject(); + return new SkinImpl(texture.get("value").getAsString(), texture.get("signature").getAsString()); + } + + } catch (IOException exception) { + if (!configManager.getConfig().disableSkinFetcherWarnings()) { + logger.warning("Failed to get skin from file:"); + exception.printStackTrace(); + } + } finally { + if (connection != null) connection.disconnect(); + } + return null; + }); + } + + public boolean isNameFullyCached(String s) { + String name = s.toLowerCase(); + if (!idCache.containsKey(name)) return false; + CachedId id = idCache.get(name); + if (id.isExpired() || !cache.containsKey(id.getId())) return false; + SkinImpl skin = cache.get(id.getId()); + return !skin.isExpired(); + } + + public SkinImpl getFullyCachedByName(String s) { + String name = s.toLowerCase(); + if (!idCache.containsKey(name)) return null; + CachedId id = idCache.get(name); + if (id.isExpired() || !cache.containsKey(id.getId())) return null; + SkinImpl skin = cache.get(id.getId()); + if (skin.isExpired()) return null; + return skin; + } + + public CompletableFuture fetchByUUID(String uuid) { + Player player = Bukkit.getPlayer(uuid); + if (player != null && player.isOnline()) return CompletableFuture.completedFuture(getFromPlayer(player)); + + if (cache.containsKey(uuid)) { + SkinImpl skin = cache.get(uuid); + if (!skin.isExpired()) return CompletableFuture.completedFuture(skin); + } + + return FutureUtil.exceptionPrintingSupplyAsync(() -> { + URL url = parseUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false"); + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + try (Reader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { + JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject(); + if (obj.has("errorMessage")) return null; + SkinImpl skin = new SkinImpl(obj); + cache.put(uuid, skin); + return skin; + } + } catch (IOException exception) { + if (!configManager.getConfig().disableSkinFetcherWarnings()) { + logger.warning("Failed to fetch skin, trying to use fallback server:"); + exception.printStackTrace(); + } + } finally { + if (connection != null) connection.disconnect(); + } + return null; + }); + } + + public SkinImpl getFromPlayer(Player player) { + try { + Object playerHandle = Reflections.GET_PLAYER_HANDLE_METHOD.get().invoke(player); + Object gameProfile = Reflections.GET_PROFILE_METHOD.get().invoke(playerHandle); + Object propertyMap = Reflections.GET_PROPERTY_MAP_METHOD.get().invoke(gameProfile); + return new SkinImpl(propertyMap); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private static URL parseUrl(String url) { + try { + return new URL(url); + } catch (MalformedURLException exception) { + throw new RuntimeException(exception); + } + } + + public File getSkinsFolder() { + return skinsFolder; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/SkinCacheCleanTask.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/SkinCacheCleanTask.java new file mode 100644 index 0000000..92491cd --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/SkinCacheCleanTask.java @@ -0,0 +1,16 @@ +package lol.pyr.znpcsplus.skin.cache; + +import org.bukkit.scheduler.BukkitRunnable; + +public class SkinCacheCleanTask extends BukkitRunnable { + private final MojangSkinCache skinCache; + + public SkinCacheCleanTask(MojangSkinCache skinCache) { + this.skinCache = skinCache; + } + + @Override + public void run() { + skinCache.cleanCache(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/MirrorDescriptor.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/MirrorDescriptor.java new file mode 100644 index 0000000..8971d0b --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/MirrorDescriptor.java @@ -0,0 +1,37 @@ +package lol.pyr.znpcsplus.skin.descriptor; + +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.skin.BaseSkinDescriptor; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import org.bukkit.entity.Player; + +import java.util.concurrent.CompletableFuture; + +public class MirrorDescriptor implements BaseSkinDescriptor, SkinDescriptor { + private final MojangSkinCache skinCache; + + public MirrorDescriptor(MojangSkinCache skinCache) { + this.skinCache = skinCache; + } + + @Override + public CompletableFuture fetch(Player player) { + return CompletableFuture.completedFuture(skinCache.getFromPlayer(player)); + } + + @Override + public SkinImpl fetchInstant(Player player) { + return skinCache.getFromPlayer(player); + } + + @Override + public boolean supportsInstant(Player player) { + return true; + } + + @Override + public String serialize() { + return "mirror"; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/NameFetchingDescriptor.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/NameFetchingDescriptor.java new file mode 100644 index 0000000..00d4521 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/NameFetchingDescriptor.java @@ -0,0 +1,44 @@ +package lol.pyr.znpcsplus.skin.descriptor; + +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.skin.BaseSkinDescriptor; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.util.PapiUtil; +import org.bukkit.entity.Player; + +import java.util.concurrent.CompletableFuture; + +public class NameFetchingDescriptor implements BaseSkinDescriptor, SkinDescriptor { + private final MojangSkinCache skinCache; + private final String name; + + public NameFetchingDescriptor(MojangSkinCache skinCache, String name) { + this.skinCache = skinCache; + this.name = name; + } + + @Override + public CompletableFuture fetch(Player player) { + return skinCache.fetchByName(PapiUtil.set(player, name)); + } + + @Override + public SkinImpl fetchInstant(Player player) { + return skinCache.getFullyCachedByName(PapiUtil.set(player, name)); + } + + @Override + public boolean supportsInstant(Player player) { + return skinCache.isNameFullyCached(PapiUtil.set(player, name)); + } + + public String getName() { + return name; + } + + @Override + public String serialize() { + return "fetching;" + name; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/PrefetchedDescriptor.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/PrefetchedDescriptor.java new file mode 100644 index 0000000..b263cc2 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/PrefetchedDescriptor.java @@ -0,0 +1,70 @@ +package lol.pyr.znpcsplus.skin.descriptor; + +import com.github.retrooper.packetevents.protocol.player.TextureProperty; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.skin.BaseSkinDescriptor; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.util.FutureUtil; +import org.bukkit.entity.Player; + +import java.io.FileNotFoundException; +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +public class PrefetchedDescriptor implements BaseSkinDescriptor, SkinDescriptor { + private final SkinImpl skin; + + public PrefetchedDescriptor(SkinImpl skin) { + this.skin = skin; + } + + public static CompletableFuture forPlayer(MojangSkinCache cache, String name) { + return FutureUtil.exceptionPrintingSupplyAsync(() -> new PrefetchedDescriptor(cache.fetchByName(name).join())); + } + + public static CompletableFuture fromUrl(MojangSkinCache cache, URL url, String variant) { + return FutureUtil.exceptionPrintingSupplyAsync(() -> new PrefetchedDescriptor(cache.fetchByUrl(url, variant).join())); + } + + public static CompletableFuture fromFile(MojangSkinCache cache, String path) { + return CompletableFuture.supplyAsync(() -> { + try { + return new PrefetchedDescriptor(cache.fetchFromFile(path).join()); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public CompletableFuture fetch(Player player) { + return CompletableFuture.completedFuture(skin); + } + + @Override + public SkinImpl fetchInstant(Player player) { + return skin; + } + + @Override + public boolean supportsInstant(Player player) { + return true; + } + + public SkinImpl getSkin() { + return skin; + } + + @Override + public String serialize() { + StringBuilder sb = new StringBuilder(); + sb.append("prefetched;"); + for (TextureProperty property : skin.getProperties()) { + sb.append(property.getName()).append(";"); + sb.append(property.getValue()).append(";"); + sb.append(property.getSignature()).append(";"); + } + return sb.toString(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/UUIDFetchingDescriptor.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/UUIDFetchingDescriptor.java new file mode 100644 index 0000000..685c786 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/UUIDFetchingDescriptor.java @@ -0,0 +1,42 @@ +package lol.pyr.znpcsplus.skin.descriptor; + +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.skin.BaseSkinDescriptor; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import org.bukkit.entity.Player; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class UUIDFetchingDescriptor implements BaseSkinDescriptor, SkinDescriptor { + + private final MojangSkinCache skinCache; + private final UUID uuid; + + public UUIDFetchingDescriptor(MojangSkinCache skinCache, UUID uuid) { + this.skinCache = skinCache; + this.uuid = uuid; + } + + @Override + public CompletableFuture fetch(Player player) { + return skinCache.fetchByUUID(uuid.toString()); + } + + @Override + public SkinImpl fetchInstant(Player player) { + return fetch(player).join(); + } + + @Override + public boolean supportsInstant(Player player) { + return false; + } + + @Override + public String serialize() { + return "fetching-uuid;" + uuid.toString(); + } + +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorage.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorage.java new file mode 100644 index 0000000..702d9ca --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorage.java @@ -0,0 +1,14 @@ +package lol.pyr.znpcsplus.storage; + +import lol.pyr.znpcsplus.npc.NpcEntryImpl; + +import java.util.Collection; + +public interface NpcStorage { + Collection loadNpcs(); + void saveNpcs(Collection npcs); + void deleteNpc(NpcEntryImpl npc); + default void close() { + + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorageType.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorageType.java new file mode 100644 index 0000000..b4139cc --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorageType.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.storage; + +import lol.pyr.znpcsplus.ZNpcsPlus; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.serialization.NpcSerializerRegistryImpl; +import lol.pyr.znpcsplus.storage.mysql.MySQLStorage; +import lol.pyr.znpcsplus.storage.sqlite.SQLiteStorage; +import lol.pyr.znpcsplus.storage.yaml.YamlStorage; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.io.File; + +public enum NpcStorageType { + YAML { + @Override + public NpcStorage create(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, NpcSerializerRegistryImpl serializerRegistry) { + return new YamlStorage(serializerRegistry, new File(plugin.getDataFolder(), "data")); + } + }, + SQLITE { + @Override + public NpcStorage create(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, NpcSerializerRegistryImpl serializerRegistry) { + try { + return new SQLiteStorage(packetFactory, configManager, actionRegistry, typeRegistry, propertyRegistry, textSerializer, new File(plugin.getDataFolder(), "znpcsplus.sqlite")); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + }, + MYSQL { + @Override + public NpcStorage create(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, NpcSerializerRegistryImpl serializerRegistry) { + try { + return new MySQLStorage(packetFactory, configManager, actionRegistry, typeRegistry, propertyRegistry, textSerializer); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + }; + + public abstract NpcStorage create(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, NpcSerializerRegistryImpl serializerRegistry); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/database/Database.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/database/Database.java new file mode 100644 index 0000000..4e41696 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/database/Database.java @@ -0,0 +1,18 @@ +package lol.pyr.znpcsplus.storage.database; + +import java.sql.Connection; +import java.util.logging.Logger; + +public abstract class Database { + protected final Logger logger; + protected Connection connection; + public Database(Logger logger){ + this.logger = logger; + } + + public abstract Connection getSQLConnection(); + + public abstract void load(); + + public abstract void close(); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/mysql/MySQL.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/mysql/MySQL.java new file mode 100644 index 0000000..ccdf79d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/mysql/MySQL.java @@ -0,0 +1,131 @@ +package lol.pyr.znpcsplus.storage.mysql; + +import lol.pyr.znpcsplus.storage.database.Database; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.logging.Logger; + +public class MySQL extends Database { + private final String connectionURL; + private final String username; + private final String password; + + public MySQL(String connectionURL, String username, String password, Logger logger) { + super(logger); + this.connectionURL = connectionURL; + this.username = username; + this.password = password; + } + + @Override + public Connection getSQLConnection() { + validateConnectionUrl(); + + try { + if (connection != null && !connection.isClosed()) { + return connection; + } + Class.forName("com.mysql.jdbc.Driver"); + connection = java.sql.DriverManager.getConnection(connectionURL, username, password); + return connection; + } catch (ClassNotFoundException ex) { + logger.severe("MySQL JDBC library not found" + ex); + } catch (SQLException ex) { + if (ex.getSQLState().equals("08006")) { + logger.severe("Could not connect to MySQL server. Check your connection settings and make sure the server is online."); + } else if (ex.getSQLState().equals("08002")) { + logger.severe("A connection already exists." + ex); + } else { + logger.severe("MySQL exception on initialize" + ex); + } + } + return null; + } + + private void validateConnectionUrl() { + if (connectionURL == null || connectionURL.isEmpty()) { + throw new IllegalArgumentException("Connection URL cannot be null or empty"); + } + if (!connectionURL.startsWith("jdbc:mysql://")) { + throw new IllegalArgumentException("Connection URL must start with 'jdbc:mysql://'"); + } + // TODO: Validate the rest of the URL + } + + @Override + public void load() { + connection = getSQLConnection(); + } + + @Override + public void close() { + try { + if (connection != null) { + connection.close(); + } + } catch (SQLException e) { + logger.severe("An error occurred while closing the connection"); + e.printStackTrace(); + } + } + + public boolean tableExists(String tableName) { + try { + Statement s = connection.createStatement(); + s.executeQuery("SELECT * FROM " + tableName + ";"); + s.close(); + return true; + } catch (SQLException e) { + return false; + } + } + + public boolean columnExists(String tableName, String columnName) { + try { + Statement s = connection.createStatement(); + s.executeQuery("SELECT " + columnName + " FROM " + tableName + ";"); + s.close(); + return true; + } catch (SQLException e) { + return false; + } + } + + public boolean addColumn(String tableName, String columnName, String type) { + if (columnExists(tableName, columnName)) return false; + try { + Statement s = connection.createStatement(); + s.executeQuery("ALTER TABLE " + tableName + " ADD COLUMN " + columnName + " " + type + ";"); + s.close(); + } catch (SQLException e) { + return false; + } + return true; + } + + public ResultSet executeQuery(String query) { + try { + Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery(query); + s.close(); + return rs; + } catch (SQLException e) { + return null; + } + } + + public int executeUpdate(String sql) { + try { + Statement s = connection.createStatement(); + int rowCount = s.executeUpdate(sql); + s.close(); + return rowCount; + } catch (SQLException e) { + e.printStackTrace(); + return -1; + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/mysql/MySQLStorage.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/mysql/MySQLStorage.java new file mode 100644 index 0000000..b7fe0cb --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/mysql/MySQLStorage.java @@ -0,0 +1,322 @@ +package lol.pyr.znpcsplus.storage.mysql; + +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.storage.NpcStorage; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.math.BigDecimal; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class MySQLStorage implements NpcStorage { + private final static Logger logger = Logger.getLogger("MySQLStorage"); + + private final PacketFactory packetFactory; + private final ConfigManager configManager; + private final ActionRegistryImpl actionRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final LegacyComponentSerializer textSerializer; + private final MySQL database; + + private final String TABLE_NPCS; + private final String TABLE_NPCS_PROPERTIES; + private final String TABLE_NPCS_HOLOGRAMS; + private final String TABLE_NPCS_ACTIONS; + + public MySQLStorage(PacketFactory packetFactory, ConfigManager configManager, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer) { + this.packetFactory = packetFactory; + this.configManager = configManager; + this.actionRegistry = actionRegistry; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.textSerializer = textSerializer; + this.database = new MySQL(configManager.getConfig().databaseConfig().createConnectionURL("mysql"), + configManager.getConfig().databaseConfig().username(), configManager.getConfig().databaseConfig().password(), logger); + database.load(); + if (database.getSQLConnection() == null) { + throw new RuntimeException("Failed to initialize MySQL Storage"); + } + TABLE_NPCS = "npcs"; + TABLE_NPCS_PROPERTIES = "npcs_properties"; + TABLE_NPCS_HOLOGRAMS = "npcs_holograms"; + TABLE_NPCS_ACTIONS = "npcs_actions"; + validateTables(); + } + + private void validateTables() { + if (!database.tableExists(TABLE_NPCS)) { + logger.info("Creating table " + TABLE_NPCS + "..."); + createNpcsTable(); + } + if (!database.tableExists(TABLE_NPCS_PROPERTIES)) { + logger.info("Creating table " + TABLE_NPCS_PROPERTIES + "..."); + createNpcsPropertiesTable(); + } + if (!database.tableExists(TABLE_NPCS_HOLOGRAMS)) { + logger.info("Creating table " + TABLE_NPCS_HOLOGRAMS + "..."); + createNpcsHologramsTable(); + } + if (!database.tableExists(TABLE_NPCS_ACTIONS)) { + logger.info("Creating table " + TABLE_NPCS_ACTIONS + "..."); + createNpcsActionsTable(); + } + updateTables(); + } + + private void createNpcsTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS + + " (id VARCHAR(256) PRIMARY KEY, isProcessed BOOLEAN, allowCommands BOOLEAN, enabled BOOLEAN, " + + "uuid VARCHAR(36), world VARCHAR(128), x DOUBLE, y DOUBLE, z DOUBLE, yaw DOUBLE, pitch DOUBLE, type VARCHAR(128), hologramOffset DOUBLE, hologramRefreshDelay BIGINT)") != -1) { + logger.info("Table " + TABLE_NPCS + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS + "."); + } + } + + private void createNpcsPropertiesTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS_PROPERTIES + + " (npc_id VARCHAR(256), property VARCHAR(128), value TEXT, PRIMARY KEY (npc_id, property))") != -1) { + logger.info("Table " + TABLE_NPCS_PROPERTIES + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS_PROPERTIES + "."); + } + } + + private void createNpcsHologramsTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS_HOLOGRAMS + + " (npc_id VARCHAR(256), line INT, text TEXT, PRIMARY KEY (npc_id, line))") != -1) { + logger.info("Table " + TABLE_NPCS_HOLOGRAMS + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS_HOLOGRAMS + "."); + } + } + + private void createNpcsActionsTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS_ACTIONS + + " (npc_id VARCHAR(256), action_id INT, action_data TEXT, PRIMARY KEY (npc_id, action_id))") != -1) { + logger.info("Table " + TABLE_NPCS_ACTIONS + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS_ACTIONS + "."); + } + } + + private void updateTables() { + // Any table updates go here + } + + @Override + public Collection loadNpcs() { + Map npcMap = new HashMap<>(); + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS); + ResultSet rs = st.executeQuery(); + while (rs.next()) { + NpcImpl npc = new NpcImpl(UUID.fromString(rs.getString("uuid")), propertyRegistry, configManager, packetFactory, textSerializer, + rs.getString("world"), typeRegistry.getByName(rs.getString("type")), + new NpcLocation(rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z"), rs.getFloat("yaw"), rs.getFloat("pitch"))); + + if (!rs.getBoolean("enabled")) npc.setEnabled(false); + + npc.getHologram().setOffset(rs.getDouble("hologramOffset")); + if (rs.getBigDecimal("hologramRefreshDelay") != null) npc.getHologram().setRefreshDelay(rs.getBigDecimal("hologramRefreshDelay").longValue()); + + NpcEntryImpl entry = new NpcEntryImpl(rs.getString("id"), npc); + entry.setProcessed(rs.getBoolean("isProcessed")); + entry.setAllowCommandModification(rs.getBoolean("allowCommands")); + entry.setSave(true); + npcMap.put(rs.getString("id"), entry); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS_PROPERTIES); + ResultSet rs = st.executeQuery(); + while (rs.next()) { + NpcEntryImpl entry = npcMap.get(rs.getString("npc_id")); + String key = rs.getString("property"); + if (entry != null) { + EntityPropertyImpl property = propertyRegistry.getByName(key); + if (property == null) { + logger.warning("Unknown property '" + key + "' for npc '" + rs.getString("npc_id") + "'. skipping ..."); + continue; + } + PropertySerializer serializer = propertyRegistry.getSerializer(property.getType()); + if (serializer == null) { + logger.warning("Unknown serializer for property '" + key + "' for npc '" + rs.getString("npc_id") + "'. skipping ..."); + continue; + } + Object value = serializer.deserialize(rs.getString("value")); + if (value == null) { + logger.warning("Failed to deserialize property '" + key + "' for npc '" + rs.getString("npc_id") + "'. Resetting to default ..."); + value = property.getDefaultValue(); + } + entry.getNpc().UNSAFE_setProperty(property, value); + npcMap.put(rs.getString("npc_id"), entry); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS_HOLOGRAMS + " ORDER BY line"); + ResultSet rs = st.executeQuery(); + + while (rs.next()) { + NpcEntryImpl entry = npcMap.get(rs.getString("npc_id")); + if (entry != null) { + entry.getNpc().getHologram().insertLine(rs.getInt("line"), rs.getString("text")); + } + npcMap.put(rs.getString("npc_id"), entry); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS_ACTIONS + " ORDER BY action_id"); + ResultSet rs = st.executeQuery(); + + while (rs.next()) { + NpcEntryImpl entry = npcMap.get(rs.getString("npc_id")); + if (entry != null) { + entry.getNpc().addAction(actionRegistry.deserialize(rs.getString("action_data"))); + } + npcMap.put(rs.getString("npc_id"), entry); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + return npcMap.values().stream().filter(Objects::nonNull).collect(Collectors.toList()); + } + + @Override + public void saveNpcs(Collection npcs) { + long start = System.currentTimeMillis(); + for (NpcEntryImpl entry : npcs) try { + + PreparedStatement ps; + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS + " (id, isProcessed, allowCommands, enabled, uuid, world, x, y, z, yaw, pitch, type, hologramOffset, hologramRefreshDelay) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); + ps.setString(1, entry.getId()); + ps.setBoolean(2, entry.isProcessed()); + ps.setBoolean(3, entry.isAllowCommandModification()); + NpcImpl npc = entry.getNpc(); + ps.setBoolean(4, npc.isEnabled()); + ps.setString(5, npc.getUuid().toString()); + ps.setString(6, npc.getWorldName()); + ps.setDouble(7, npc.getLocation().getX()); + ps.setDouble(8, npc.getLocation().getY()); + ps.setDouble(9, npc.getLocation().getZ()); + ps.setFloat(10, npc.getLocation().getYaw()); + ps.setFloat(11, npc.getLocation().getPitch()); + ps.setString(12, npc.getType().getName()); + HologramImpl hologram = npc.getHologram(); + ps.setDouble(13, hologram.getOffset()); + ps.setBigDecimal(14, new BigDecimal(hologram.getRefreshDelay())); + + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_PROPERTIES + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + for (EntityProperty property : npc.getAllProperties()) try { + PropertySerializer serializer = propertyRegistry.getSerializer(((EntityPropertyImpl) property).getType()); + if (serializer == null) { + logger.warning("Unknown serializer for property '" + property.getName() + "' for npc '" + entry.getId() + "'. skipping ..."); + continue; + } + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS_PROPERTIES + " (npc_id, property, value) VALUES(?,?,?)"); + ps.setString(1, entry.getId()); + ps.setString(2, property.getName()); + ps.setString(3, serializer.UNSAFE_serialize(npc.getProperty(property))); + ps.executeUpdate(); + } catch (Exception exception) { + logger.severe("Failed to serialize property " + property.getName() + " for npc with id " + entry.getId()); + exception.printStackTrace(); + } + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_HOLOGRAMS + " WHERE npc_id = ? AND line > ?"); + ps.setString(1, entry.getId()); + ps.setInt(2, hologram.getLines().size() - 1); + ps.executeUpdate(); + + for (int i = 0; i < hologram.getLines().size(); i++) { + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS_HOLOGRAMS + " (npc_id, line, text) VALUES(?,?,?)"); + ps.setString(1, entry.getId()); + ps.setInt(2, i); + ps.setString(3, hologram.getLine(i)); + ps.executeUpdate(); + } + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_ACTIONS + " WHERE npc_id = ? AND action_id > ?"); + ps.setString(1, entry.getId()); + ps.setInt(2, npc.getActions().size() - 1); + ps.executeUpdate(); + + for (int i = 0; i < npc.getActions().size(); i++) { + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS_ACTIONS + " (npc_id, action_id, action_data) VALUES(?,?,?)"); + ps.setString(1, entry.getId()); + ps.setInt(2, i); + String action = actionRegistry.serialize(npc.getActions().get(i)); + if (action == null) continue; + ps.setString(3, action); + ps.executeUpdate(); + } + } catch (SQLException exception) { + logger.severe("Failed to save npc with id " + entry.getId()); + exception.printStackTrace(); + } + if (configManager.getConfig().debugEnabled()) { + logger.info("Saved " + npcs.size() + " npcs in " + (System.currentTimeMillis() - start) + "ms"); + } + } + + @Override + public void deleteNpc(NpcEntryImpl entry) { + try { + PreparedStatement ps; + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS + " WHERE id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_PROPERTIES + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_HOLOGRAMS + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_ACTIONS + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + } catch (SQLException exception) { + logger.severe("Failed to delete npc with id " + entry.getId()); + exception.printStackTrace(); + } + } + + @Override + public void close() { + database.close(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLite.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLite.java new file mode 100644 index 0000000..ff1a273 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLite.java @@ -0,0 +1,112 @@ +package lol.pyr.znpcsplus.storage.sqlite; + +import lol.pyr.znpcsplus.storage.database.Database; + +import java.io.File; +import java.io.IOException; +import java.sql.*; +import java.util.logging.Logger; + +public class SQLite extends Database{ + private final File dbFile; + public SQLite(File file, Logger logger){ + super(logger); + dbFile = file; + } + + public Connection getSQLConnection() { + if (!dbFile.exists()){ + try { + dbFile.createNewFile(); + } catch (IOException e) { + logger.severe("File write error: "+dbFile.getName()); + } + } + try { + if(connection!=null&&!connection.isClosed()){ + return connection; + } + Class.forName("org.sqlite.JDBC"); + connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath()); + return connection; + } catch (SQLException ex) { + logger.severe("SQLite exception on initialize" + ex); + } catch (ClassNotFoundException ex) { + logger.severe("SQLite JDBC library not found" + ex); + } + return null; + } + + public void load() { + connection = getSQLConnection(); + } + + @Override + public void close() { + try { + if (connection != null) { + connection.close(); + } + } catch (SQLException e) { + logger.severe("An error occurred while closing the connection"); + e.printStackTrace(); + } + } + + public boolean tableExists(String tableName) { + try { + Statement s = connection.createStatement(); + s.executeQuery("SELECT * FROM " + tableName + ";"); + s.close(); + return true; + } catch (SQLException e) { + return false; + } + } + + public boolean columnExists(String tableName, String columnName) { + try { + Statement s = connection.createStatement(); + s.executeQuery("SELECT " + columnName + " FROM " + tableName + ";"); + s.close(); + return true; + } catch (SQLException e) { + return false; + } + } + + public boolean addColumn(String tableName, String columnName, String type) { + if (columnExists(tableName, columnName)) return false; + try { + Statement s = connection.createStatement(); + s.executeQuery("ALTER TABLE " + tableName + " ADD COLUMN " + columnName + " " + type + ";"); + s.close(); + } catch (SQLException e) { + return false; + } + return true; + } + + public ResultSet executeQuery(String query) { + try { + Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery(query); + s.close(); + return rs; + } catch (SQLException e) { + return null; + } + } + + public int executeUpdate(String query) { + try { + Statement s = connection.createStatement(); + int rowCount = s.executeUpdate(query); + s.close(); + return rowCount; + } catch (SQLException e) { + e.printStackTrace(); + return -1; + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLiteStorage.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLiteStorage.java new file mode 100644 index 0000000..0dc7592 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLiteStorage.java @@ -0,0 +1,320 @@ +package lol.pyr.znpcsplus.storage.sqlite; + +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.storage.NpcStorage; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.io.File; +import java.math.BigDecimal; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class SQLiteStorage implements NpcStorage { + private final static Logger logger = Logger.getLogger("SQLiteStorage"); + + private final PacketFactory packetFactory; + private final ConfigManager configManager; + private final ActionRegistryImpl actionRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final LegacyComponentSerializer textSerializer; + private final SQLite database; + + private final String TABLE_NPCS; + private final String TABLE_NPCS_PROPERTIES; + private final String TABLE_NPCS_HOLOGRAMS; + private final String TABLE_NPCS_ACTIONS; + + public SQLiteStorage(PacketFactory packetFactory, ConfigManager configManager, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, File file) { + this.packetFactory = packetFactory; + this.configManager = configManager; + this.actionRegistry = actionRegistry; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.textSerializer = textSerializer; + this.database = new SQLite(file, logger); + database.load(); + if (database.getSQLConnection() == null) { + throw new RuntimeException("Failed to initialize SQLite Storage."); + } + TABLE_NPCS = "npcs"; + TABLE_NPCS_PROPERTIES = "npcs_properties"; + TABLE_NPCS_HOLOGRAMS = "npcs_holograms"; + TABLE_NPCS_ACTIONS = "npcs_actions"; + validateTables(); + } + + private void validateTables() { + if (!database.tableExists(TABLE_NPCS)) { + logger.info("Creating table " + TABLE_NPCS + "..."); + createNpcsTable(); + } + if (!database.tableExists(TABLE_NPCS_PROPERTIES)) { + logger.info("Creating table " + TABLE_NPCS_PROPERTIES + "..."); + createNpcsPropertiesTable(); + } + if (!database.tableExists(TABLE_NPCS_HOLOGRAMS)) { + logger.info("Creating table " + TABLE_NPCS_HOLOGRAMS + "..."); + createNpcsHologramsTable(); + } + if (!database.tableExists(TABLE_NPCS_ACTIONS)) { + logger.info("Creating table " + TABLE_NPCS_ACTIONS + "..."); + createNpcsActionsTable(); + } + updateTables(); + } + + private void createNpcsTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS + " " + + "(id TEXT PRIMARY KEY, isProcessed BOOLEAN, allowCommands BOOLEAN, enabled BOOLEAN, " + + "uuid TEXT, world TEXT, x REAL, y REAL, z REAL, yaw REAL, pitch REAL, type TEXT, hologramOffset REAL, hologramRefreshDelay INTEGER)") != -1) { + logger.info("Table " + TABLE_NPCS + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS + "."); + } + } + + private void createNpcsPropertiesTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS_PROPERTIES + " " + + "(npc_id TEXT, property TEXT, value TEXT, PRIMARY KEY (npc_id, property))") != -1) { + logger.info("Table " + TABLE_NPCS_PROPERTIES + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS_PROPERTIES + "."); + } + } + + private void createNpcsHologramsTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS_HOLOGRAMS + " " + + "(npc_id TEXT, line INTEGER, text TEXT, PRIMARY KEY (npc_id, line))") != -1) { + logger.info("Table " + TABLE_NPCS_HOLOGRAMS + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS_HOLOGRAMS + "."); + } + } + + private void createNpcsActionsTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS_ACTIONS + " " + + "(npc_id TEXT, action_id INTEGER, action_data TEXT, PRIMARY KEY (npc_id, action_id))") != -1) { + logger.info("Table " + TABLE_NPCS_ACTIONS + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS_ACTIONS + "."); + } + } + + private void updateTables() { + // Any table updates go here + } + + @Override + public Collection loadNpcs() { + Map npcMap = new HashMap<>(); + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS); + ResultSet rs = st.executeQuery(); + while (rs.next()) { + NpcImpl npc = new NpcImpl(UUID.fromString(rs.getString("uuid")), propertyRegistry, configManager, packetFactory, textSerializer, + rs.getString("world"), typeRegistry.getByName(rs.getString("type")), + new NpcLocation(rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z"), rs.getFloat("yaw"), rs.getFloat("pitch"))); + + if (!rs.getBoolean("enabled")) npc.setEnabled(false); + + npc.getHologram().setOffset(rs.getDouble("hologramOffset")); + if (rs.getBigDecimal("hologramRefreshDelay") != null) npc.getHologram().setRefreshDelay(rs.getBigDecimal("hologramRefreshDelay").longValue()); + + NpcEntryImpl entry = new NpcEntryImpl(rs.getString("id"), npc); + entry.setProcessed(rs.getBoolean("isProcessed")); + entry.setAllowCommandModification(rs.getBoolean("allowCommands")); + entry.setSave(true); + npcMap.put(rs.getString("id"), entry); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS_PROPERTIES); + ResultSet rs = st.executeQuery(); + while (rs.next()) { + NpcEntryImpl entry = npcMap.get(rs.getString("npc_id")); + String key = rs.getString("property"); + if (entry != null) { + EntityPropertyImpl property = propertyRegistry.getByName(key); + if (property == null) { + logger.warning("Unknown property '" + key + "' for npc '" + rs.getString("npc_id") + "'. skipping ..."); + continue; + } + PropertySerializer serializer = propertyRegistry.getSerializer(property.getType()); + if (serializer == null) { + logger.warning("Unknown serializer for property '" + key + "' for npc '" + rs.getString("npc_id") + "'. skipping ..."); + continue; + } + Object value = serializer.deserialize(rs.getString("value")); + if (value == null) { + logger.warning("Failed to deserialize property '" + key + "' for npc '" + rs.getString("npc_id") + "'. Resetting to default ..."); + value = property.getDefaultValue(); + } + entry.getNpc().UNSAFE_setProperty(property, value); + npcMap.put(rs.getString("npc_id"), entry); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS_HOLOGRAMS + " ORDER BY line"); + ResultSet rs = st.executeQuery(); + + while (rs.next()) { + NpcEntryImpl entry = npcMap.get(rs.getString("npc_id")); + if (entry != null) { + entry.getNpc().getHologram().insertLine(rs.getInt("line"), rs.getString("text")); + } + npcMap.put(rs.getString("npc_id"), entry); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS_ACTIONS + " ORDER BY action_id"); + ResultSet rs = st.executeQuery(); + + while (rs.next()) { + NpcEntryImpl entry = npcMap.get(rs.getString("npc_id")); + if (entry != null) { + entry.getNpc().addAction(actionRegistry.deserialize(rs.getString("action_data"))); + } + npcMap.put(rs.getString("npc_id"), entry); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + return npcMap.values().stream().filter(Objects::nonNull).collect(Collectors.toList()); + } + + @Override + public void saveNpcs(Collection npcs) { + long start = System.currentTimeMillis(); + for (NpcEntryImpl entry : npcs) try { + PreparedStatement ps; + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS + " (id, isProcessed, allowCommands, enabled, uuid, world, x, y, z, yaw, pitch, type, hologramOffset, hologramRefreshDelay) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); + ps.setString(1, entry.getId()); + ps.setBoolean(2, entry.isProcessed()); + ps.setBoolean(3, entry.isAllowCommandModification()); + NpcImpl npc = entry.getNpc(); + ps.setBoolean(4, npc.isEnabled()); + ps.setString(5, npc.getUuid().toString()); + ps.setString(6, npc.getWorldName()); + ps.setDouble(7, npc.getLocation().getX()); + ps.setDouble(8, npc.getLocation().getY()); + ps.setDouble(9, npc.getLocation().getZ()); + ps.setFloat(10, npc.getLocation().getYaw()); + ps.setFloat(11, npc.getLocation().getPitch()); + ps.setString(12, npc.getType().getName()); + HologramImpl hologram = npc.getHologram(); + ps.setDouble(13, hologram.getOffset()); + ps.setBigDecimal(14, new BigDecimal(hologram.getRefreshDelay())); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_PROPERTIES + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + for (EntityProperty property : npc.getAllProperties()) try { + PropertySerializer serializer = propertyRegistry.getSerializer(((EntityPropertyImpl) property).getType()); + if (serializer == null) { + logger.warning("Unknown serializer for property '" + property.getName() + "' for npc '" + entry.getId() + "'. skipping ..."); + continue; + } + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS_PROPERTIES + " (npc_id, property, value) VALUES(?,?,?)"); + ps.setString(1, entry.getId()); + ps.setString(2, property.getName()); + ps.setString(3, serializer.UNSAFE_serialize(npc.getProperty(property))); + ps.executeUpdate(); + } catch (Exception exception) { + logger.severe("Failed to serialize property " + property.getName() + " for npc with id " + entry.getId()); + exception.printStackTrace(); + } + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_HOLOGRAMS + " WHERE npc_id = ? AND line > ?"); + ps.setString(1, entry.getId()); + ps.setInt(2, hologram.getLines().size() - 1); + ps.executeUpdate(); + + for (int i = 0; i < hologram.getLines().size(); i++) { + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS_HOLOGRAMS + " (npc_id, line, text) VALUES(?,?,?)"); + ps.setString(1, entry.getId()); + ps.setInt(2, i); + ps.setString(3, hologram.getLine(i)); + ps.executeUpdate(); + } + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_ACTIONS + " WHERE npc_id = ? AND action_id > ?"); + ps.setString(1, entry.getId()); + ps.setInt(2, npc.getActions().size() - 1); + ps.executeUpdate(); + + for (int i = 0; i < npc.getActions().size(); i++) { + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS_ACTIONS + " (npc_id, action_id, action_data) VALUES(?,?,?)"); + ps.setString(1, entry.getId()); + ps.setInt(2, i); + String action = actionRegistry.serialize(npc.getActions().get(i)); + if (action == null) continue; + ps.setString(3, action); + ps.executeUpdate(); + } + } catch (SQLException exception) { + logger.severe("Failed to save npc with id " + entry.getId()); + exception.printStackTrace(); + } + if (configManager.getConfig().debugEnabled()) { + logger.info("Saved " + npcs.size() + " npcs in " + (System.currentTimeMillis() - start) + "ms"); + } + } + + @Override + public void deleteNpc(NpcEntryImpl entry) { + try { + PreparedStatement ps; + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS + " WHERE id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_PROPERTIES + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_HOLOGRAMS + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_ACTIONS + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + } catch (SQLException exception) { + logger.severe("Failed to delete npc with id " + entry.getId()); + exception.printStackTrace(); + } + } + + @Override + public void close() { + database.close(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/yaml/YamlStorage.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/yaml/YamlStorage.java new file mode 100644 index 0000000..b985fa7 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/yaml/YamlStorage.java @@ -0,0 +1,84 @@ +package lol.pyr.znpcsplus.storage.yaml; + +import lol.pyr.znpcsplus.api.serialization.NpcSerializer; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.serialization.NpcSerializerRegistryImpl; +import lol.pyr.znpcsplus.storage.NpcStorage; +import lol.pyr.znpcsplus.util.NpcLocation; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +public class YamlStorage implements NpcStorage { + private final static Logger logger = Logger.getLogger("YamlStorage"); + + private final File folder; + private final NpcSerializer yamlSerializer; + + public YamlStorage(NpcSerializerRegistryImpl serializerRegistry, File folder) { + this.yamlSerializer = serializerRegistry.getSerializer(YamlConfiguration.class); + this.folder = folder; + if (!this.folder.exists()) this.folder.mkdirs(); + } + + @Override + public Collection loadNpcs() { + File[] files = folder.listFiles(); + if (files == null || files.length == 0) return Collections.emptyList(); + List npcs = new ArrayList<>(files.length); + for (File file : files) if (file.isFile() && file.getName().toLowerCase().endsWith(".yml")) try { + YamlConfiguration config = YamlConfiguration.loadConfiguration(file); + npcs.add((NpcEntryImpl) yamlSerializer.deserialize(config)); + } catch (Throwable t) { + logger.severe("Failed to load npc file: " + file.getName()); + t.printStackTrace(); + } + return npcs; + } + + @Override + public void saveNpcs(Collection npcs) { + for (NpcEntryImpl entry : npcs) try { + YamlConfiguration config = yamlSerializer.serialize(entry); + config.save(fileFor(entry)); + } catch (Exception exception) { + logger.severe("Failed to save npc with id " + entry.getId()); + exception.printStackTrace(); + } + } + + @Override + public void deleteNpc(NpcEntryImpl npc) { + fileFor(npc).delete(); + } + + private File fileFor(NpcEntryImpl entry) { + return new File(folder, entry.getId() + ".yml"); + } + + public NpcLocation deserializeLocation(ConfigurationSection section) { + return new NpcLocation( + section.getDouble("x"), + section.getDouble("y"), + section.getDouble("z"), + (float) section.getDouble("yaw"), + (float) section.getDouble("pitch") + ); + } + + public YamlConfiguration serializeLocation(NpcLocation location) { + YamlConfiguration config = new YamlConfiguration(); + config.set("x", location.getX()); + config.set("y", location.getY()); + config.set("z", location.getZ()); + config.set("yaw", location.getYaw()); + config.set("pitch", location.getPitch()); + return config; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/tasks/HologramRefreshTask.java b/plugin/src/main/java/lol/pyr/znpcsplus/tasks/HologramRefreshTask.java new file mode 100644 index 0000000..9139f68 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/tasks/HologramRefreshTask.java @@ -0,0 +1,22 @@ +package lol.pyr.znpcsplus.tasks; + +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import org.bukkit.scheduler.BukkitRunnable; + +public class HologramRefreshTask extends BukkitRunnable { + private final NpcRegistryImpl npcRegistry; + + public HologramRefreshTask(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run() { + for (NpcEntryImpl entry : npcRegistry.getProcessable()) { + HologramImpl hologram = entry.getNpc().getHologram(); + if (hologram.shouldRefresh()) hologram.refresh(); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/tasks/NpcProcessorTask.java b/plugin/src/main/java/lol/pyr/znpcsplus/tasks/NpcProcessorTask.java new file mode 100644 index 0000000..a975546 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/tasks/NpcProcessorTask.java @@ -0,0 +1,150 @@ +package lol.pyr.znpcsplus.tasks; + +import lol.pyr.znpcsplus.api.event.NpcDespawnEvent; +import lol.pyr.znpcsplus.api.event.NpcSpawnEvent; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.user.User; +import lol.pyr.znpcsplus.user.UserManager; +import lol.pyr.znpcsplus.util.LookType; +import lol.pyr.znpcsplus.util.NpcLocation; +import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.util.NumberConversions; +import org.bukkit.util.Vector; + +public class NpcProcessorTask extends BukkitRunnable { + private final NpcRegistryImpl npcRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final UserManager userManager; + + public NpcProcessorTask(NpcRegistryImpl npcRegistry, EntityPropertyRegistryImpl propertyRegistry,UserManager userManager) { + this.npcRegistry = npcRegistry; + this.propertyRegistry = propertyRegistry; + this.userManager = userManager; + } + + public void run() { + EntityPropertyImpl viewDistanceProperty = propertyRegistry.getByName("view_distance", Integer.class); // Not sure why this is an Integer, but it is + EntityPropertyImpl lookProperty = propertyRegistry.getByName("look", LookType.class); + EntityPropertyImpl lookDistanceProperty = propertyRegistry.getByName("look_distance", Double.class); + EntityPropertyImpl lookReturnProperty = propertyRegistry.getByName("look_return", Boolean.class); + EntityPropertyImpl permissionRequiredProperty = propertyRegistry.getByName("permission_required", Boolean.class); + EntityPropertyImpl playerKnockbackProperty = propertyRegistry.getByName("player_knockback", Boolean.class); + EntityPropertyImpl playerKnockbackExemptPermissionProperty = propertyRegistry.getByName("player_knockback_exempt_permission", String.class); + EntityPropertyImpl playerKnockbackDistanceProperty = propertyRegistry.getByName("player_knockback_distance", Double.class); + EntityPropertyImpl playerKnockbackVerticalProperty = propertyRegistry.getByName("player_knockback_vertical", Double.class); + EntityPropertyImpl playerKnockbackHorizontalProperty = propertyRegistry.getByName("player_knockback_horizontal", Double.class); + EntityPropertyImpl playerKnockbackCooldownProperty = propertyRegistry.getByName("player_knockback_cooldown", Integer.class); + EntityPropertyImpl playerKnockbackSoundProperty = propertyRegistry.getByName("player_knockback_sound", Boolean.class); + EntityPropertyImpl playerKnockbackSoundNameProperty = propertyRegistry.getByName("player_knockback_sound_name", Sound.class); + EntityPropertyImpl playerKnockbackSoundVolumeProperty = propertyRegistry.getByName("player_knockback_sound_volume", Float.class); + EntityPropertyImpl playerKnockbackSoundPitchProperty = propertyRegistry.getByName("player_knockback_sound_pitch", Float.class); + double lookDistance; + boolean lookReturn; + boolean permissionRequired; + boolean playerKnockback; + String playerKnockbackExemptPermission = null; + double playerKnockbackDistance = 0; + double playerKnockbackVertical = 0; + double playerKnockbackHorizontal = 0; + int playerKnockbackCooldown = 0; + boolean playerKnockbackSound = false; + Sound playerKnockbackSoundName = null; + float playerKnockbackSoundVolume = 0; + float playerKnockbackSoundPitch = 0; + for (NpcEntryImpl entry : npcRegistry.getProcessable()) { + NpcImpl npc = entry.getNpc(); + if (!npc.isEnabled()) continue; + + double closestDist = Double.MAX_VALUE; + Player closest = null; + LookType lookType = npc.getProperty(lookProperty); + lookDistance = NumberConversions.square(npc.getProperty(lookDistanceProperty)); + lookReturn = npc.getProperty(lookReturnProperty); + permissionRequired = npc.getProperty(permissionRequiredProperty); + playerKnockback = npc.getProperty(playerKnockbackProperty); + if (playerKnockback) { + playerKnockbackExemptPermission = npc.getProperty(playerKnockbackExemptPermissionProperty); + playerKnockbackDistance = NumberConversions.square(npc.getProperty(playerKnockbackDistanceProperty)); + playerKnockbackVertical = npc.getProperty(playerKnockbackVerticalProperty); + playerKnockbackHorizontal = npc.getProperty(playerKnockbackHorizontalProperty); + playerKnockbackCooldown = npc.getProperty(playerKnockbackCooldownProperty); + playerKnockbackSound = npc.getProperty(playerKnockbackSoundProperty); + playerKnockbackSoundName = npc.getProperty(playerKnockbackSoundNameProperty); + playerKnockbackSoundVolume = npc.getProperty(playerKnockbackSoundVolumeProperty); + playerKnockbackSoundPitch = npc.getProperty(playerKnockbackSoundPitchProperty); + } + for (Player player : Bukkit.getOnlinePlayers()) { + if (!player.getWorld().equals(npc.getWorld())) { + if (npc.isVisibleTo(player)) npc.hide(player); + continue; + } + if (permissionRequired && !player.hasPermission("znpcsplus.npc." + entry.getId())) { + if (npc.isVisibleTo(player)) npc.hide(player); + continue; + } + double distance = player.getLocation().distanceSquared(npc.getBukkitLocation()); + + // visibility + boolean inRange = distance <= NumberConversions.square(npc.getProperty(viewDistanceProperty)); + if (!inRange && npc.isVisibleTo(player)) { + NpcDespawnEvent event = new NpcDespawnEvent(player, entry); + Bukkit.getPluginManager().callEvent(event); + if (!event.isCancelled()) npc.hide(player); + } + if (inRange) { + if (!npc.isVisibleTo(player)) { + NpcSpawnEvent event = new NpcSpawnEvent(player, entry); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) continue; + npc.show(player); + } + if (distance < closestDist) { + closestDist = distance; + closest = player; + } + if (lookType.equals(LookType.PER_PLAYER)) { + if (lookDistance >= distance) { + NpcLocation expected = npc.getLocation().lookingAt(player.getLocation().add(0, -npc.getType().getHologramOffset(), 0)); + npc.setHeadRotation(player, expected.getYaw(), expected.getPitch()); + } else if (lookReturn) { + npc.setHeadRotation(player, npc.getLocation().getYaw(), npc.getLocation().getPitch()); + } + } + + // player knockback + User user = userManager.get(player.getUniqueId()); + if (playerKnockbackExemptPermission == null || !player.hasPermission(playerKnockbackExemptPermission)) { + if (playerKnockback && distance <= playerKnockbackDistance && user.canKnockback(playerKnockbackCooldown)) { + double x = npc.getLocation().getX() - player.getLocation().getX(); + double z = npc.getLocation().getZ() - player.getLocation().getZ(); + double angle = Math.atan2(z, x); + double knockbackX = -Math.cos(angle) * playerKnockbackHorizontal; + double knockbackZ = -Math.sin(angle) * playerKnockbackHorizontal; + player.setVelocity(player.getVelocity().add(new Vector(knockbackX, playerKnockbackVertical, knockbackZ))); + if (playerKnockbackSound) + player.playSound(player.getLocation(), playerKnockbackSoundName, playerKnockbackSoundVolume, playerKnockbackSoundPitch); + } + } + } + } + // look property + if (lookType.equals(LookType.CLOSEST_PLAYER)) { + if (closest != null && lookDistance >= closestDist) { + NpcLocation expected = npc.getLocation().lookingAt(closest.getLocation().add(0, -npc.getType().getHologramOffset(), 0)); + if (!expected.equals(npc.getLocation())) npc.setHeadRotation(expected.getYaw(), expected.getPitch()); + } else if (lookReturn) { + npc.setHeadRotation(npc.getLocation().getYaw(), npc.getLocation().getPitch()); + } + } else if (lookType.equals(LookType.FIXED)) { + npc.setHeadRotation(npc.getLocation().getYaw(), npc.getLocation().getPitch()); + } + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/tasks/ViewableHideOnLeaveListener.java b/plugin/src/main/java/lol/pyr/znpcsplus/tasks/ViewableHideOnLeaveListener.java new file mode 100644 index 0000000..a411711 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/tasks/ViewableHideOnLeaveListener.java @@ -0,0 +1,15 @@ +package lol.pyr.znpcsplus.tasks; + +import lol.pyr.znpcsplus.util.Viewable; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; + +public class ViewableHideOnLeaveListener implements Listener { + @EventHandler + public void onQuit(PlayerQuitEvent event) { + Viewable.all().forEach(viewable -> { + if (viewable.isVisibleTo(event.getPlayer())) viewable.UNSAFE_removeViewer(event.getPlayer()); + }); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateChecker.java b/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateChecker.java new file mode 100644 index 0000000..ef6ecc1 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateChecker.java @@ -0,0 +1,116 @@ +package lol.pyr.znpcsplus.updater; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.scheduler.BukkitRunnable; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.logging.Logger; + +public class UpdateChecker extends BukkitRunnable { + private final static Logger logger = Logger.getLogger("ZNPCsPlus Update Checker"); + private final static String GET_RESOURCE = "https://api.spigotmc.org/simple/0.2/index.php?action=getResource&id=109380"; + public final static String DOWNLOAD_LINK = "https://www.spigotmc.org/resources/znpcsplus.109380/"; + + private final PluginDescriptionFile info; + private Status status = Status.UNKNOWN; + private String newestVersion = "N/A"; + + public UpdateChecker(PluginDescriptionFile info) { + this.info = info; + } + + public void run() { + String foundVersion = null; + try { + URL getResource = new URL(GET_RESOURCE); + HttpURLConnection httpRequest = ((HttpURLConnection) getResource.openConnection()); + httpRequest.setRequestMethod("GET"); + httpRequest.setConnectTimeout(5_000); + httpRequest.setReadTimeout(5_000); + + if (httpRequest.getResponseCode() == HttpURLConnection.HTTP_OK) { + try (InputStreamReader reader = new InputStreamReader(httpRequest.getInputStream())) { + JsonObject jsonObject = JsonParser.parseReader(reader).getAsJsonObject(); + foundVersion = jsonObject.get("current_version").getAsString(); + } + } else { + logger.warning("Failed to check for updates: HTTP response code " + httpRequest.getResponseCode()); + } + } catch (IOException e) { + logger.warning("Failed to check for updates: " + e.getMessage()); + return; + } + + if (foundVersion == null) return; + newestVersion = foundVersion; + + status = compareVersions(info.getVersion(), newestVersion); + if (status == Status.UPDATE_NEEDED) notifyConsole(); + } + + private void notifyConsole() { + logger.warning("Version " + getLatestVersion() + " of " + info.getName() + " is available now!"); + logger.warning("Download it at " + UpdateChecker.DOWNLOAD_LINK); + } + + private Status compareVersions(String currentVersion, String newVersion) { + if (currentVersion.equalsIgnoreCase(newVersion)) return Status.LATEST_VERSION; + ReleaseType currentType = parseReleaseType(currentVersion); + ReleaseType newType = parseReleaseType(newVersion); + if (currentType == ReleaseType.UNKNOWN || newType == ReleaseType.UNKNOWN) return Status.UNKNOWN; + String currentVersionWithoutType = getVersionWithoutReleaseType(currentVersion); + String newVersionWithoutType = getVersionWithoutReleaseType(newVersion); + String[] currentParts = currentVersionWithoutType.split("\\."); + String[] newParts = newVersionWithoutType.split("\\."); + for (int i = 0; i < Math.min(currentParts.length, newParts.length); i++) { + int currentPart = Integer.parseInt(currentParts[i]); + int newPart = Integer.parseInt(newParts[i]); + if (newPart > currentPart) return Status.UPDATE_NEEDED; + if (newPart < currentPart) return Status.LATEST_VERSION; + } + if (newType.ordinal() > currentType.ordinal()) return Status.UPDATE_NEEDED; + if (newType == currentType) { + int currentReleaseTypeNumber = getReleaseTypeNumber(currentVersion); + int newReleaseTypeNumber = getReleaseTypeNumber(newVersion); + if (newReleaseTypeNumber > currentReleaseTypeNumber) return Status.UPDATE_NEEDED; + } + return Status.LATEST_VERSION; + } + + private ReleaseType parseReleaseType(String version) { + if (version.toLowerCase().contains("snapshot")) return ReleaseType.SNAPSHOT; + if (version.toLowerCase().contains("alpha")) return ReleaseType.ALPHA; + if (version.toLowerCase().contains("beta")) return ReleaseType.BETA; + return version.matches("\\d+\\.\\d+\\.\\d+") ? ReleaseType.RELEASE : ReleaseType.UNKNOWN; + } + + private String getVersionWithoutReleaseType(String version) { + return version.contains("-") ? version.split("-")[0] : version; + } + + private int getReleaseTypeNumber(String version) { + if (!version.contains("-")) return 0; + return Integer.parseInt(version.split("-")[1].split("\\.")[1]); + } + + public Status getStatus() { + return status; + } + + public String getLatestVersion() { + return newestVersion; + } + + public enum Status { + UNKNOWN, LATEST_VERSION, UPDATE_NEEDED + } + + public enum ReleaseType { + UNKNOWN, SNAPSHOT, ALPHA, BETA, RELEASE + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateNotificationListener.java b/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateNotificationListener.java new file mode 100644 index 0000000..f3b2493 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateNotificationListener.java @@ -0,0 +1,37 @@ +package lol.pyr.znpcsplus.updater; + +import lol.pyr.znpcsplus.ZNpcsPlus; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +public class UpdateNotificationListener implements Listener { + private final ZNpcsPlus plugin; + private final BukkitAudiences adventure; + private final UpdateChecker updateChecker; + private final TaskScheduler scheduler; + + public UpdateNotificationListener(ZNpcsPlus plugin, BukkitAudiences adventure, UpdateChecker updateChecker, TaskScheduler scheduler) { + this.plugin = plugin; + this.adventure = adventure; + this.updateChecker = updateChecker; + this.scheduler = scheduler; + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + if (!event.getPlayer().hasPermission("znpcsplus.updates")) return; + if (updateChecker.getStatus() != UpdateChecker.Status.UPDATE_NEEDED) return; + scheduler.runLaterAsync(() -> { + if (!event.getPlayer().isOnline()) return; + adventure.player(event.getPlayer()) + .sendMessage(Component.text(plugin.getDescription().getName() + " v" + updateChecker.getLatestVersion() + " is available now!", NamedTextColor.GOLD).appendNewline() + .append(Component.text("Click this message to open the Spigot page (CLICK)", NamedTextColor.YELLOW)).clickEvent(ClickEvent.openUrl(UpdateChecker.DOWNLOAD_LINK))); + }, 100L); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/user/ClientPacketListener.java b/plugin/src/main/java/lol/pyr/znpcsplus/user/ClientPacketListener.java new file mode 100644 index 0000000..9580597 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/user/ClientPacketListener.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.user; + +import com.github.retrooper.packetevents.event.PacketListener; +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerJoinGame; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerServerData; +import lol.pyr.znpcsplus.config.ConfigManager; + +public class ClientPacketListener implements PacketListener { + private final ConfigManager configManager; + + public ClientPacketListener(ConfigManager configManager) { + this.configManager = configManager; + } + + @Override + public void onPacketSend(PacketSendEvent event) { + if (!configManager.getConfig().fakeEnforceSecureChat()) return; + if (event.getPacketType() == PacketType.Play.Server.SERVER_DATA) { + WrapperPlayServerServerData packet = new WrapperPlayServerServerData(event); + packet.setEnforceSecureChat(true); + event.setByteBuf(packet.getBuffer()); + } else if (event.getPacketType() == PacketType.Play.Server.JOIN_GAME) { + WrapperPlayServerJoinGame packet = new WrapperPlayServerJoinGame(event); + packet.setEnforcesSecureChat(true); + event.setByteBuf(packet.getBuffer()); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/user/User.java b/plugin/src/main/java/lol/pyr/znpcsplus/user/User.java new file mode 100644 index 0000000..216001e --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/user/User.java @@ -0,0 +1,53 @@ +package lol.pyr.znpcsplus.user; + +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class User { + private final UUID uuid; + private long lastNpcInteraction; + private long lastNpcKnockback; + private final Map actionCooldownMap = new HashMap<>(); + + public User(UUID uuid) { + this.uuid = uuid; + } + + public Player getPlayer() { + return Bukkit.getPlayer(uuid); + } + + public boolean canInteract() { + if (System.currentTimeMillis() - lastNpcInteraction > 100L) { + lastNpcInteraction = System.currentTimeMillis(); + return true; + } + return false; + } + + public boolean canKnockback(int cooldown) { + if (System.currentTimeMillis() - lastNpcKnockback > cooldown) { + lastNpcKnockback = System.currentTimeMillis(); + return true; + } + return false; + } + + public UUID getUuid() { + return uuid; + } + + public boolean actionCooldownCheck(InteractionAction action) { + UUID id = action.getUuid(); + if (System.currentTimeMillis() - actionCooldownMap.getOrDefault(id, 0L) >= action.getCooldown()) { + actionCooldownMap.put(id, System.currentTimeMillis()); + return true; + } + return false; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/user/UserListener.java b/plugin/src/main/java/lol/pyr/znpcsplus/user/UserListener.java new file mode 100644 index 0000000..266a81b --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/user/UserListener.java @@ -0,0 +1,24 @@ +package lol.pyr.znpcsplus.user; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +public class UserListener implements Listener { + private final UserManager manager; + + public UserListener(UserManager manager) { + this.manager = manager; + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + manager.get(event.getPlayer()); + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + manager.remove(event.getPlayer().getUniqueId()); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/user/UserManager.java b/plugin/src/main/java/lol/pyr/znpcsplus/user/UserManager.java new file mode 100644 index 0000000..ee0c12b --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/user/UserManager.java @@ -0,0 +1,36 @@ +package lol.pyr.znpcsplus.user; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class UserManager { + private final Map userMap = new ConcurrentHashMap<>(); + + public UserManager() { + Bukkit.getOnlinePlayers().forEach(this::get); + } + + public User get(Player player) { + return get(player.getUniqueId()); + } + + public User get(UUID uuid) { + return userMap.computeIfAbsent(uuid, User::new); + } + + public void remove(Player player) { + remove(player.getUniqueId()); + } + + public void remove(UUID uuid) { + userMap.remove(uuid); + } + + public void shutdown() { + Bukkit.getOnlinePlayers().forEach(this::remove); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/BungeeConnector.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/BungeeConnector.java new file mode 100644 index 0000000..18193c7 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/BungeeConnector.java @@ -0,0 +1,35 @@ +package lol.pyr.znpcsplus.util; + +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +public class BungeeConnector { + private final static String CHANNEL_NAME = "BungeeCord"; + private final Plugin plugin; + + public BungeeConnector(Plugin plugin) { + this.plugin = plugin; + } + + public void connectPlayer(Player player, String server) { + player.sendPluginMessage(plugin, CHANNEL_NAME, createMessage("Connect", server)); + } + + @SuppressWarnings("UnstableApiUsage") + private byte[] createMessage(String... parts) { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + for (String part : parts) out.writeUTF(part); + return out.toByteArray(); + } + + public void registerChannel() { + Bukkit.getMessenger().registerOutgoingPluginChannel(plugin, CHANNEL_NAME); + } + + public void unregisterChannel() { + Bukkit.getMessenger().unregisterOutgoingPluginChannel(plugin, CHANNEL_NAME); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/FileUtil.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/FileUtil.java new file mode 100644 index 0000000..1bf3197 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/FileUtil.java @@ -0,0 +1,23 @@ +package lol.pyr.znpcsplus.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; + +public class FileUtil { + public static String dumpReaderAsString(Reader reader) { + BufferedReader bReader = new BufferedReader(reader); + try { + List lines = new ArrayList<>(); + String line; + while ((line = bReader.readLine()) != null) { + lines.add(line); + } + return String.join("\n", lines); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/FoliaUtil.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/FoliaUtil.java new file mode 100644 index 0000000..54c010b --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/FoliaUtil.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.util; + +import lol.pyr.znpcsplus.reflection.Reflections; +import org.bukkit.Location; +import org.bukkit.entity.Entity; + +import java.lang.reflect.InvocationTargetException; + +public class FoliaUtil { + private static final Boolean FOLIA = isFolia(); + public static boolean isFolia() { + if (FOLIA != null) return FOLIA; + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + public static void teleport(Entity entity, Location location) { + if (!isFolia()) entity.teleport(location); + else try { + Reflections.FOLIA_TELEPORT_ASYNC.get().invoke(entity, location); + } catch (IllegalAccessException | InvocationTargetException e) { + System.err.println("Error while teleporting entity:"); + e.printStackTrace(); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/FutureUtil.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/FutureUtil.java new file mode 100644 index 0000000..18ca6e4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/FutureUtil.java @@ -0,0 +1,34 @@ +package lol.pyr.znpcsplus.util; + +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +public class FutureUtil { + public static CompletableFuture allOf(Collection> futures) { + return exceptionPrintingRunAsync(() -> { + for (CompletableFuture future : futures) future.join(); + }); + } + + public static CompletableFuture newExceptionPrintingFuture() { + return new CompletableFuture().exceptionally(throwable -> { + throwable.printStackTrace(); + return null; + }); + } + + public static CompletableFuture exceptionPrintingRunAsync(Runnable runnable) { + return CompletableFuture.runAsync(runnable).exceptionally(throwable -> { + throwable.printStackTrace(); + return null; + }); + } + + public static CompletableFuture exceptionPrintingSupplyAsync(Supplier supplier) { + return CompletableFuture.supplyAsync(supplier).exceptionally(throwable -> { + throwable.printStackTrace(); + return null; + }); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/ItemSerializationUtil.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/ItemSerializationUtil.java new file mode 100644 index 0000000..ed5f866 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/ItemSerializationUtil.java @@ -0,0 +1,47 @@ +package lol.pyr.znpcsplus.util; + +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; + +public class ItemSerializationUtil { + public static byte[] objectToBytes(Object obj) { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + try { + new BukkitObjectOutputStream(bout).writeObject(obj); + } catch (IOException e) { + throw new RuntimeException(e); + } + return bout.toByteArray(); + } + + @SuppressWarnings({"unchecked", "unused"}) + public static T objectFromBytes(byte[] bytes, Class clazz) { + ByteArrayInputStream bin = new ByteArrayInputStream(bytes); + try { + return (T) new BukkitObjectInputStream(bin).readObject(); + } catch (IOException | ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + public static String itemToB64(ItemStack item) { + if (item == null) return null; + return Base64.getEncoder().encodeToString(objectToBytes(item)); + } + + public static ItemStack itemFromB64(String str) { + if (str == null) return null; + return objectFromBytes(Base64.getDecoder().decode(str), ItemStack.class); + } + + public static ItemMeta metaFromB64(String str) { + return objectFromBytes(Base64.getDecoder().decode(str), ItemMeta.class); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/LazyLoader.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/LazyLoader.java new file mode 100644 index 0000000..509022f --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/LazyLoader.java @@ -0,0 +1,21 @@ +package lol.pyr.znpcsplus.util; + +import java.util.function.Supplier; + +public class LazyLoader { + private final Supplier supplier; + private T value; + + private LazyLoader(Supplier supplier) { + this.supplier = supplier; + } + + public T get() { + if (value == null) value = supplier.get(); + return value; + } + + public static LazyLoader of(Supplier supplier) { + return new LazyLoader<>(supplier); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/PapiUtil.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/PapiUtil.java new file mode 100644 index 0000000..f64d63b --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/PapiUtil.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.util; + +import me.clip.placeholderapi.PlaceholderAPI; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public class PapiUtil { + private static boolean isSupported() { + return Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI"); + } + + public static String set(String str) { + return set(null, str); + } + + public static String set(Player player, String str) { + return isSupported() ? PlaceholderAPI.setPlaceholders(player, str) : str; + } + + // Ugly workaround would be cool if a better solution existed + public static Component set(LegacyComponentSerializer serializer, Player player, Component component) { + if (!isSupported()) return component; + return serializer.deserialize(set(player, serializer.serialize(component))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/Viewable.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/Viewable.java new file mode 100644 index 0000000..cc222a8 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/Viewable.java @@ -0,0 +1,123 @@ +package lol.pyr.znpcsplus.util; + +import org.bukkit.entity.Player; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.stream.Collectors; + +public abstract class Viewable { + private final static List> all = Collections.synchronizedList(new ArrayList<>()); + + public static List all() { + synchronized (all) { + all.removeIf(reference -> reference.get() == null); + return all.stream() + .map(Reference::get) + .collect(Collectors.toList()); + } + } + + private boolean queueRunning = false; + private final Queue visibilityTaskQueue = new ConcurrentLinkedQueue<>(); + private final Set viewers = ConcurrentHashMap.newKeySet(); + + public Viewable() { + all.add(new WeakReference<>(this)); + } + + private void tryRunQueue() { + if (visibilityTaskQueue.isEmpty() || queueRunning) return; + queueRunning = true; + FutureUtil.exceptionPrintingRunAsync(() -> { + while (!visibilityTaskQueue.isEmpty()) try { + visibilityTaskQueue.remove().run(); + } catch (Exception e) { + e.printStackTrace(); + } + queueRunning = false; + }); + } + + private void queueVisibilityTask(Runnable runnable) { + visibilityTaskQueue.add(runnable); + tryRunQueue(); + } + + public void delete() { + queueVisibilityTask(() -> { + UNSAFE_hideAll(); + viewers.clear(); + synchronized (all) { + all.removeIf(reference -> reference.get() == null || reference.get() == this); + } + }); + } + + public CompletableFuture respawn() { + CompletableFuture future = new CompletableFuture<>(); + queueVisibilityTask(() -> { + UNSAFE_hideAll(); + UNSAFE_showAll().join(); + future.complete(null); + }); + return future; + } + + public CompletableFuture respawn(Player player) { + hide(player); + return show(player); + } + + public CompletableFuture show(Player player) { + CompletableFuture future = new CompletableFuture<>(); + queueVisibilityTask(() -> { + if (viewers.contains(player)) { + future.complete(null); + return; + } + viewers.add(player); + UNSAFE_show(player).join(); + future.complete(null); + }); + return future; + } + + public void hide(Player player) { + queueVisibilityTask(() -> { + if (!viewers.contains(player)) return; + viewers.remove(player); + UNSAFE_hide(player); + }); + } + + public void UNSAFE_removeViewer(Player player) { + viewers.remove(player); + } + + protected void UNSAFE_hideAll() { + for (Player viewer : viewers) UNSAFE_hide(viewer); + } + + protected CompletableFuture UNSAFE_showAll() { + return FutureUtil.allOf(viewers.stream() + .map(this::UNSAFE_show) + .collect(Collectors.toList())); + } + + public Set getViewers() { + return Collections.unmodifiableSet(viewers); + } + + public boolean isVisibleTo(Player player) { + return viewers.contains(player); + } + + protected abstract CompletableFuture UNSAFE_show(Player player); + + protected abstract void UNSAFE_hide(Player player); +} diff --git a/plugin/src/main/resources/messages/action-hover/add.txt b/plugin/src/main/resources/messages/action-hover/add.txt new file mode 100644 index 0000000..23b0083 --- /dev/null +++ b/plugin/src/main/resources/messages/action-hover/add.txt @@ -0,0 +1,13 @@ +Examples: + * /npc action add consolecommand cool_npc1 ANY_CLICK 0 0 say {player} just clicked a cool npc! + * /npc action add playerchat dog LEFT_CLICK 0 100 It has been 5 seconds since i clicked the npc + * /npc action add message npc123 RIGHT_CLICK 1 0 You can only click this npc once per second + +Action Types: + * Console Command - Send a console command when a player interacts with the npc + * Message - Send a message to any player that interacts with the npc + * Player Chat - Make any player that interacts send something in the chat + * Player Command - Make any player that interacts send a command + * Switch Server - Send the player to a different server on the proxy using bungee messaging channel + +Command used to add actions to an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/action-hover/clear.txt b/plugin/src/main/resources/messages/action-hover/clear.txt new file mode 100644 index 0000000..ef981f7 --- /dev/null +++ b/plugin/src/main/resources/messages/action-hover/clear.txt @@ -0,0 +1,3 @@ +Usage » /npc action clear + +Command used to clear all npc actions \ No newline at end of file diff --git a/plugin/src/main/resources/messages/action-hover/delete.txt b/plugin/src/main/resources/messages/action-hover/delete.txt new file mode 100644 index 0000000..5a6507e --- /dev/null +++ b/plugin/src/main/resources/messages/action-hover/delete.txt @@ -0,0 +1,3 @@ +Usage » /npc action delete + +Command used to delete a specific action from an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/action-hover/edit.txt b/plugin/src/main/resources/messages/action-hover/edit.txt new file mode 100644 index 0000000..1827560 --- /dev/null +++ b/plugin/src/main/resources/messages/action-hover/edit.txt @@ -0,0 +1,3 @@ +Usage » /npc action edit + +Command used to change a specific action on an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/action-hover/list.txt b/plugin/src/main/resources/messages/action-hover/list.txt new file mode 100644 index 0000000..7397e5f --- /dev/null +++ b/plugin/src/main/resources/messages/action-hover/list.txt @@ -0,0 +1,3 @@ +Usage » /npc action list + +Command used to list all actions of an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/action.txt b/plugin/src/main/resources/messages/action.txt new file mode 100644 index 0000000..f6e1e09 --- /dev/null +++ b/plugin/src/main/resources/messages/action.txt @@ -0,0 +1,10 @@ + +ZNPCsPlus v${version} Click to view the main help message'>[BACK] +Hover over any command for more info + + * /npc action add + * /npc action clear + * /npc action delete + * /npc action edit + * /npc action list + diff --git a/plugin/src/main/resources/messages/holo-hover/add.txt b/plugin/src/main/resources/messages/holo-hover/add.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/additem.txt b/plugin/src/main/resources/messages/holo-hover/additem.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/delete.txt b/plugin/src/main/resources/messages/holo-hover/delete.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/info.txt b/plugin/src/main/resources/messages/holo-hover/info.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/insert.txt b/plugin/src/main/resources/messages/holo-hover/insert.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/insertitem.txt b/plugin/src/main/resources/messages/holo-hover/insertitem.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/offset.txt b/plugin/src/main/resources/messages/holo-hover/offset.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/refreshdelay.txt b/plugin/src/main/resources/messages/holo-hover/refreshdelay.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/set.txt b/plugin/src/main/resources/messages/holo-hover/set.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/setitem.txt b/plugin/src/main/resources/messages/holo-hover/setitem.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo.txt b/plugin/src/main/resources/messages/holo.txt new file mode 100644 index 0000000..c4e0394 --- /dev/null +++ b/plugin/src/main/resources/messages/holo.txt @@ -0,0 +1,18 @@ + +ZNPCsPlus v${version} Click to view the main help message'>[BACK] +Hover over any command more info + + * /npc holo add + * /npc holo set + * /npc holo insert + + * /npc holo additem + * /npc holo setitem + * /npc holo insertitem + + * /npc holo delete + + * /npc holo offset + * /npc holo refreshdelay + * /npc holo info + diff --git a/plugin/src/main/resources/messages/property-hover/remove.txt b/plugin/src/main/resources/messages/property-hover/remove.txt new file mode 100644 index 0000000..20f6830 --- /dev/null +++ b/plugin/src/main/resources/messages/property-hover/remove.txt @@ -0,0 +1,3 @@ +Usage » /npc property remove + +Command used to unset properties on npcs \ No newline at end of file diff --git a/plugin/src/main/resources/messages/property-hover/set.txt b/plugin/src/main/resources/messages/property-hover/set.txt new file mode 100644 index 0000000..9f2888e --- /dev/null +++ b/plugin/src/main/resources/messages/property-hover/set.txt @@ -0,0 +1,3 @@ +Usage » /npc property set + +Command used to customize npcs with custom properties \ No newline at end of file diff --git a/plugin/src/main/resources/messages/property.txt b/plugin/src/main/resources/messages/property.txt new file mode 100644 index 0000000..33524c6 --- /dev/null +++ b/plugin/src/main/resources/messages/property.txt @@ -0,0 +1,7 @@ + +ZNPCsPlus v${version} Click to view the main help message'>[BACK] +Hover over any command more info + + * /npc property set + * /npc property remove + diff --git a/plugin/src/main/resources/messages/root-hover/center.txt b/plugin/src/main/resources/messages/root-hover/center.txt new file mode 100644 index 0000000..eb10c05 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/center.txt @@ -0,0 +1,3 @@ +Usage » /npc center + +Command used to move an npc to the center of the block it''s currently occupying \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/changeid.txt b/plugin/src/main/resources/messages/root-hover/changeid.txt new file mode 100644 index 0000000..a819ff2 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/changeid.txt @@ -0,0 +1,3 @@ +Usage » /npc changeid + +Command used to change the id of an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/create.txt b/plugin/src/main/resources/messages/root-hover/create.txt new file mode 100644 index 0000000..4eb58b7 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/create.txt @@ -0,0 +1,3 @@ +Usage » /npc create + +Command used to create an npc of a given type \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/delete.txt b/plugin/src/main/resources/messages/root-hover/delete.txt new file mode 100644 index 0000000..69d0cdb --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/delete.txt @@ -0,0 +1,3 @@ +Usage » /npc delete + +Command used to delete an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/list.txt b/plugin/src/main/resources/messages/root-hover/list.txt new file mode 100644 index 0000000..4a2f00e --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/list.txt @@ -0,0 +1,3 @@ +Usage » /npc list + +Command used to list all npcs \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/lookatme.txt b/plugin/src/main/resources/messages/root-hover/lookatme.txt new file mode 100644 index 0000000..23c60a6 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/lookatme.txt @@ -0,0 +1,3 @@ +Usage » /npc lookatme + +Command used to set the rotation of an npc to be looking at your current location \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/move.txt b/plugin/src/main/resources/messages/root-hover/move.txt new file mode 100644 index 0000000..4edbf67 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/move.txt @@ -0,0 +1,3 @@ +Usage » /npc move + +Command used to set the location of an npc to your current location \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/near.txt b/plugin/src/main/resources/messages/root-hover/near.txt new file mode 100644 index 0000000..6129a95 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/near.txt @@ -0,0 +1,3 @@ +Usage » /npc near + +Command used to check which npcs are within a given radius around you \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/setlocation.txt b/plugin/src/main/resources/messages/root-hover/setlocation.txt new file mode 100644 index 0000000..94b16cc --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/setlocation.txt @@ -0,0 +1,3 @@ +Usage » /npc setlocation + +Command used to manually adjust an npc''s location \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/setrotation.txt b/plugin/src/main/resources/messages/root-hover/setrotation.txt new file mode 100644 index 0000000..ff97edf --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/setrotation.txt @@ -0,0 +1,3 @@ +Usage » /npc setrotation + +Command used to manually adjust an npc''s rotation \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/skin.txt b/plugin/src/main/resources/messages/root-hover/skin.txt new file mode 100644 index 0000000..8e6f756 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/skin.txt @@ -0,0 +1,13 @@ +Examples: + * /npc skin cool_npc1 static Notch + * /npc skin my_npc mirror + * /npc skin 12 dynamic %leaderboard_mining_top_1% + * /npc skin npc1234 url classic https://s.namemc.com/i/5d5eb6d84b57ea29.png + +Skin Types: + * Static - Only fetch the skin once and save the skin data + * Mirror - Copy the skin of the player who is viewing the npc + * Dynamic - Fetch the skin whenever the npc comes into viewing distance (supports placeholders) + * Url - Fetch the skin from an url to a raw skin file, this works like static + +Command used to change the skin of an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/teleport.txt b/plugin/src/main/resources/messages/root-hover/teleport.txt new file mode 100644 index 0000000..6c4a944 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/teleport.txt @@ -0,0 +1,3 @@ +Usage » /npc teleport + +Command used to teleport yourself to an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/toggle.txt b/plugin/src/main/resources/messages/root-hover/toggle.txt new file mode 100644 index 0000000..85cd1de --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/toggle.txt @@ -0,0 +1,3 @@ +Usage » /npc toggle + +Command used to enable or disable an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/type.txt b/plugin/src/main/resources/messages/root-hover/type.txt new file mode 100644 index 0000000..93fc3f1 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/type.txt @@ -0,0 +1,3 @@ +Usage » /npc type + +Command used to change the type of an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root.txt b/plugin/src/main/resources/messages/root.txt new file mode 100644 index 0000000..e7680ff --- /dev/null +++ b/plugin/src/main/resources/messages/root.txt @@ -0,0 +1,26 @@ + +ZNPCsPlus v${version} +Hover over any command for more info + + * /npc create + * /npc delete + * /npc changeid + * /npc toggle + * /npc list + * /npc type + + * /npc near + * /npc center + * /npc lookatme + * /npc setlocation + * /npc setrotation + * /npc move + * /npc teleport + + * /npc skin + + * Npc property commands
Click to view full list'>/npc property help + * Npc hologram commands
Click to view full list'>/npc holo help + * Player interaction commands
Click to view full list'>/npc action help + * Npc data storage commands
Click to view full list'>/npc storage help + diff --git a/plugin/src/main/resources/messages/storage-hover/import.txt b/plugin/src/main/resources/messages/storage-hover/import.txt new file mode 100644 index 0000000..0f758c2 --- /dev/null +++ b/plugin/src/main/resources/messages/storage-hover/import.txt @@ -0,0 +1,8 @@ +Usage » /npc storage import + +Importers: + * znpcs - Imports npcs from the ZNPCs plugin + * znpcsplus_legacy - Imports npcs from legacy versions of ZNPCsPlus + * citizens - Imports npcs from the Citizens plugin + +Command used to import npcs from a different source \ No newline at end of file diff --git a/plugin/src/main/resources/messages/storage-hover/migrate.txt b/plugin/src/main/resources/messages/storage-hover/migrate.txt new file mode 100644 index 0000000..371148c --- /dev/null +++ b/plugin/src/main/resources/messages/storage-hover/migrate.txt @@ -0,0 +1,16 @@ +Usage » /npc storage migrate [force] + +Storage Types: + * YAML - Npcs are stored in yaml files + * SQLite - Npcs are stored in a SQLite database + * MySQL - Npcs are stored in a MySQL database + +Command used to migrate npcs from one storage type to another. + +This command will NOT delete the original storage files or database, +but will copy the npcs to the new storage type. + +This will also not overwrite any existing npcs in the new storage +type, unless the force argument is set to true. +Warning: force will overwrite any existing npcs with the same id +in the new storage type and CANNOT be undone. \ No newline at end of file diff --git a/plugin/src/main/resources/messages/storage-hover/reload.txt b/plugin/src/main/resources/messages/storage-hover/reload.txt new file mode 100644 index 0000000..45d05a7 --- /dev/null +++ b/plugin/src/main/resources/messages/storage-hover/reload.txt @@ -0,0 +1,4 @@ +Usage » /npc storage reload + +Command used to re-load all npcs from storage +Warning: This command will delete all unsaved changes to npcs \ No newline at end of file diff --git a/plugin/src/main/resources/messages/storage-hover/save.txt b/plugin/src/main/resources/messages/storage-hover/save.txt new file mode 100644 index 0000000..a255ede --- /dev/null +++ b/plugin/src/main/resources/messages/storage-hover/save.txt @@ -0,0 +1,3 @@ +Usage » /npc storage save + +Command used to save the currently loaded npcs to storage \ No newline at end of file diff --git a/plugin/src/main/resources/messages/storage.txt b/plugin/src/main/resources/messages/storage.txt new file mode 100644 index 0000000..6cfcc3f --- /dev/null +++ b/plugin/src/main/resources/messages/storage.txt @@ -0,0 +1,9 @@ + +ZNPCsPlus v${version} Click to view the main help message'>[BACK] +Hover over any command for more info + + * /npc storage save + * /npc storage reload + * /npc storage import + * /npc storage migrate [force] + diff --git a/plugin/src/main/resources/plugin.yml b/plugin/src/main/resources/plugin.yml new file mode 100644 index 0000000..44dc2d4 --- /dev/null +++ b/plugin/src/main/resources/plugin.yml @@ -0,0 +1,33 @@ +name: ZNPCsPlus +authors: + - Pyr + - D3v1s0m + +main: lol.pyr.znpcsplus.ZNpcsPlusBootstrap +load: POSTWORLD + +version: ${version} +api-version: 1.13 + +folia-supported: true + +softdepend: + - PlaceholderAPI + - ServersNPC + - ProtocolLib + - ProtocolSupport + - ViaVersion + - ViaBackwards + - ViaRewind + - Geyser-Spigot + +loadbefore: + - Quests + +commands: + npc: + aliases: + - znpc + - znpcs + - npcs + permission: znpcsplus.command.npc diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..13c6ed6 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = "ZNPCsPlus" + +include "api", "plugin" \ No newline at end of file