commit 88f246b48ee23f7dfb99c5df1bc93f09f0f677b1 Author: bunchy7s Date: Sun Jun 21 19:09:57 2026 -0500 upload current workin files diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..988e7f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Gradle build output +build/ +.gradle/ + +# IDE files +.idea/ +*.iml +.vscode/ +.project +.classpath +.settings/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Local test server (Folia/Paper instances) +foila/ + +# Logs +*.log + +# Gradle wrapper jar (keep for reproducibility) +!gradle/wrapper/gradle-wrapper.jar diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29ebfa5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fda55b7 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# BHomes + +A simple home-management plugin for Paper servers. BHomes provides persistent SQLite storage, configurable limits, teleport cooldown/delay handling, and optional LuckPerms-based group limits. + +## About + +BHomes is designed for servers that want reliable player homes without unnecessary complexity. It supports modern Paper command registration, async data operations, and configurable limits. + +## Requirements + +- Server: Paper 1.21+ +- Java: 21 +- Optional: LuckPerms (for group-based home limits) + +## Installation + +1. Build or download the plugin JAR. +2. Place the JAR in your server `plugins/` folder. +3. Start or restart the server. +4. Edit `plugins/BHomes/config.yml` as needed. +5. Use `/bhomesreload` after config changes. + +## Commands + +### Player + +| Command | Description | +|---|---| +| `/sethome ` | Set a home at your current location | +| `/home` | List your homes | +| `/home ` | Teleport to one of your homes | +| `/delhome ` | Delete one of your homes | + +### Admin + +| Command | Description | +|---|---| +| `/homeadmin list ` | List a player's homes | +| `/homeadmin delete ` | Delete a player's home | +| `/homeadmin teleport ` | Teleport to a player's home | +| `/homeadmin export` | Export homes data to CSV | +| `/homeadmin import` | Import homes data from CSV | +| `/bhomesreload` | Reload plugin configuration | + +## Permissions + +| Permission | Default | Description | +|---|---|---| +| `bhomes.sethome` | `true` | Set homes | +| `bhomes.home` | `true` | Use `/home` | +| `bhomes.delhome` | `true` | Delete own homes | +| `bhomes.bypass.cooldown` | `op` | Bypass teleport cooldown | +| `bhomes.bypass.delay` | `op` | Bypass teleport delay | +| `bhomes.admin.list` | `op` | List player homes | +| `bhomes.admin.delete` | `op` | Delete any player's home | +| `bhomes.admin.teleport` | `op` | Teleport to any player's home | +| `bhomes.admin.reload` | `op` | Reload config | +| `bhomes.admin.import` | `op` | Import homes data | +| `bhomes.admin.export` | `op` | Export homes data | +| `bhomes.admin.*` | `op` | All admin permissions | + +## Configuration + +Main settings are in `plugins/BHomes/config.yml`: + +- `database`: storage backend and file name +- `limits`: default and group-based home limits +- `cooldown`: teleport cooldown settings +- `teleport`: delay and movement-cancel behavior +- `messages`: MiniMessage-formatted plugin messages + +## Build From Source + +```bash +./gradlew build +``` + +Compiled JAR output: + +`build/libs/` diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..337ebe8 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,146 @@ +plugins { + id("java") + id("io.papermc.paperweight.userdev") version "2.0.0-beta.14" + id("xyz.jpenilla.run-paper") version "2.3.1" +} + +fun getProperty(key: String): String { + return project.property(key) as String +} + +group = "com.bhomes" +version = getProperty("projectVersion") + +val localServerDir = "local-server" +val projectVersion = getProperty("projectVersion") + +repositories { + mavenCentral() + gradlePluginPortal() + maven { + name = "papermc" + url = uri("https://repo.papermc.io/repository/maven-public/") + } +} + +// Uncomment if the project should support Spigot on Minecraft >=1.20.5 +// paperweight.reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.REOBF_PRODUCTION + +dependencies { + // compileOnly("io.papermc.paper:paper-api:${getProperty("paperApiVersion")}") // Included with paperweight-userdev + paperweight.paperDevBundle(getProperty("paperApiVersion")) + + // SQLite Database + implementation("org.xerial:sqlite-jdbc:3.45.0.0") + + // LuckPerms API + compileOnly("net.luckperms:api:5.4") + + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +tasks { + + /********** Versioning **********/ + + fun incrementVersion(type: String) { + val (major, minor, patch) = projectVersion.split(".").map { it.toInt() } + val newVersion = when (type) { + "major" -> "${major + 1}.0.0" + "minor" -> "$major.${minor + 1}.0" + "patch" -> "$major.$minor.${patch + 1}" + else -> throw GradleException("Invalid version type") + } + file("gradle.properties").apply { + writeText(readText().replace(Regex("projectVersion=.*"), "projectVersion=$newVersion")) + } + } + + register("incrementMajorVersion") { + group = "2- versioning" + doLast { + incrementVersion("major") + } + } + + register("incrementMinorVersion") { + group = "2- versioning" + doLast { + incrementVersion("minor") + } + } + + register("incrementPatchVersion") { + group = "2- versioning" + doLast { + incrementVersion("patch") + } + } + + /********** Build plugin and run a local server **********/ + + register("buildPluginAndRunServer") { + group = "1- local server" + description = "Build the plugin's JAR file and run a Paper test server that includes it" + + dependsOn("jar") // Build plugin JAR + + doFirst { // Copy plugin JAR + val jarFile = file("build/libs/${project.name}-${version}.jar") + val pluginsDir = file("${localServerDir}/plugins").apply { mkdirs() } + + if (jarFile.exists()) + jarFile.copyTo(file("${pluginsDir}/${jarFile.name}"), overwrite = true) + else + throw GradleException("File ${jarFile.name} not found.") + } + finalizedBy("runDevBundleServer") // Run a local Paper server + } + + /** + * run-paper from https://github.com/jpenilla/run-task + * "runServer" when using Spigot mappings + * "runDevBundleServer" when using Mojang mappings (paperweight-userdev) + */ + runDevBundleServer { + group = "1- local server" + runDirectory.set(file(localServerDir)) + // minecraftVersion("1.21.4") // Automatically set by paperweight-userdev + + jvmArgs( + "-Dcom.mojang.eula.agree=true", + "-Dserver.port=25565" + ) + + doFirst { + val serverPropertiesFile = file("${localServerDir}/server.properties") + val bukkitYmlFile = file("${localServerDir}/bukkit.yml") + + listOf(serverPropertiesFile, bukkitYmlFile).forEach { + file -> file.parentFile.mkdirs() + } + + serverPropertiesFile.writeText(""" + allow-nether=false + """.trimIndent()) + + bukkitYmlFile.writeText(""" + settings: + allow-end: false + """.trimIndent()) + } + } + + named("runServer") { // Should not be used as paperweight-userdev has been implemented + throw GradleException("Please use the 'runDevBundleServer' task instead.") + } + + test { + useJUnitPlatform() + } +} + +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(21)) +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..38ec108 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +paperApiVersion=1.21.4-R0.1-SNAPSHOT +projectVersion=1.0.0 +supportedGameVersions=1.21+ +versionType=release diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 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..7e228a4 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Jan 25 19:16:13 CET 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-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/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..8a2f149 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "BHomes" \ No newline at end of file diff --git a/src/main/java/com/bhomes/plugin/BHomesPlugin.java b/src/main/java/com/bhomes/plugin/BHomesPlugin.java new file mode 100644 index 0000000..2132625 --- /dev/null +++ b/src/main/java/com/bhomes/plugin/BHomesPlugin.java @@ -0,0 +1,158 @@ +package com.bhomes.plugin; + +import com.bhomes.plugin.commands.*; +import com.bhomes.plugin.commands.admin.HomeAdminCommand; +import com.bhomes.plugin.commands.admin.ReloadCommand; +import com.bhomes.plugin.database.DatabaseManager; +import com.bhomes.plugin.managers.CooldownManager; +import com.bhomes.plugin.managers.HomeManager; +import com.bhomes.plugin.managers.PermissionManager; +import com.bhomes.plugin.managers.TeleportManager; +import com.bhomes.plugin.managers.TpaManager; +import com.bhomes.plugin.utils.ConfigManager; +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.Set; + +public class BHomesPlugin extends JavaPlugin { + + private static BHomesPlugin instance; + private DatabaseManager databaseManager; + private HomeManager homeManager; + private CooldownManager cooldownManager; + private PermissionManager permissionManager; + private TeleportManager teleportManager; + private TpaManager tpaManager; + + @Override + public void onEnable() { + instance = this; + + // Create config.yml with default values, or update missing fields based on resources/config.yml + getConfig().options().copyDefaults(true); + saveConfig(); + + // Initialize ConfigManager + ConfigManager.init(this); + + // Initialize database + this.databaseManager = new DatabaseManager(this); + if (!this.databaseManager.initialize()) { + getLogger().severe("Failed to initialize database! Disabling plugin..."); + getServer().getPluginManager().disablePlugin(this); + return; + } + + // Initialize managers + this.homeManager = new HomeManager(this); + this.cooldownManager = new CooldownManager(this); + this.permissionManager = new PermissionManager(this); + this.teleportManager = new TeleportManager(this); + this.tpaManager = new TpaManager(this); + + // Register commands + this.registerPluginCommands(); + + getLogger().info("BHomes has been enabled!"); + } + + @Override + public void onDisable() { + // Cancel all pending teleports + if (this.teleportManager != null) { + this.teleportManager.clearAllTeleports(); + } + + // Close database connection + if (this.databaseManager != null) { + this.databaseManager.close(); + } + + getLogger().info("BHomes has been disabled!"); + } + + private void registerPluginCommands() { + this.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, commands -> { + commands.registrar().register( + SetHomeCommand.createCommand(this), + SetHomeCommand.DESCRIPTION, + SetHomeCommand.ALIASES); + commands.registrar().register( + HomeCommand.createCommand(this), + HomeCommand.DESCRIPTION, + HomeCommand.ALIASES); + commands.registrar().register( + DelHomeCommand.createCommand(this), + DelHomeCommand.DESCRIPTION, + DelHomeCommand.ALIASES); + commands.registrar().register( + HomeAdminCommand.createCommand(this), + HomeAdminCommand.DESCRIPTION, + HomeAdminCommand.ALIASES); + commands.registrar().register( + ReloadCommand.createCommand(this), + ReloadCommand.DESCRIPTION, + ReloadCommand.ALIASES); + // TPA commands + commands.registrar().register( + TpaCommand.createCommand(this), + TpaCommand.DESCRIPTION, + TpaCommand.ALIASES); + commands.registrar().register( + TpaCommand.createTpaHereCommand(this), + "Request a player to teleport to you", + Set.of("tpahere")); + commands.registrar().register( + TpaCommand.createTpaBlockCommand(this), + "Block or unblock teleport requests from a player", + Set.of("tpablock")); + commands.registrar().register( + TpaCommand.createTpaAcceptCommand(this), + "Accept a teleport request", + Set.of("tpaaccept")); + commands.registrar().register( + TpaCommand.createTpAcceptCommand(this), + "Accept a teleport request", + Set.of("tpaccept")); + commands.registrar().register( + TpaCommand.createTpaDenyCommand(this), + "Deny a teleport request", + Set.of("tpadeny")); + }); + } + + public static BHomesPlugin getInstance() { + return instance; + } + + public DatabaseManager getDatabaseManager() { + return databaseManager; + } + + public HomeManager getHomeManager() { + return homeManager; + } + + public CooldownManager getCooldownManager() { + return cooldownManager; + } + + public PermissionManager getPermissionManager() { + return permissionManager; + } + + public TeleportManager getTeleportManager() { + return teleportManager; + } + + public TpaManager getTpaManager() { + return tpaManager; + } + + public void reloadPluginConfig() { + reloadConfig(); + ConfigManager.init(this); + getLogger().info("Configuration reloaded!"); + } +} diff --git a/src/main/java/com/bhomes/plugin/commands/DelHomeCommand.java b/src/main/java/com/bhomes/plugin/commands/DelHomeCommand.java new file mode 100644 index 0000000..6c39c65 --- /dev/null +++ b/src/main/java/com/bhomes/plugin/commands/DelHomeCommand.java @@ -0,0 +1,60 @@ +package com.bhomes.plugin.commands; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.bhomes.plugin.BHomesPlugin; +import com.bhomes.plugin.utils.MessageUtil; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.Set; + +public class DelHomeCommand { + + public static final String DESCRIPTION = "Delete one of your homes"; + public static final Set ALIASES = Set.of("delhome", "removehome", "deletehome"); + + private DelHomeCommand() { + } + + public static LiteralCommandNode createCommand(BHomesPlugin plugin) { + return Commands.literal("delhome") + .requires(sender -> sender.getSender().hasPermission("bhomes.delhome")) + .then(Commands.argument("name", StringArgumentType.word()) + .suggests((ctx, builder) -> { + if (ctx.getSource().getExecutor() instanceof org.bukkit.entity.Player player) { + plugin.getHomeManager().getPlayerHomesSync(player.getUniqueId()) + .forEach(home -> builder.suggest(home.getName())); + } + return builder.buildFuture(); + }) + .executes(ctx -> runDelHome(ctx, plugin))) + .build(); + } + + private static int runDelHome(CommandContext ctx, BHomesPlugin plugin) { + Entity executor = ctx.getSource().getExecutor(); + + if (!(executor instanceof Player player)) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "must-be-player"); + return Command.SINGLE_SUCCESS; + } + + String homeName = StringArgumentType.getString(ctx, "name"); + + // Delete the home directly (no GUI confirmation) + boolean success = plugin.getHomeManager().deleteHomeSync(player.getUniqueId(), homeName); + if (success) { + MessageUtil.sendMessage(player, "home-deleted", Map.of("home", homeName)); + } else { + MessageUtil.sendMessage(player, "home-not-found", Map.of("home", homeName)); + } + + return Command.SINGLE_SUCCESS; + } +} diff --git a/src/main/java/com/bhomes/plugin/commands/HomeCommand.java b/src/main/java/com/bhomes/plugin/commands/HomeCommand.java new file mode 100644 index 0000000..da3e580 --- /dev/null +++ b/src/main/java/com/bhomes/plugin/commands/HomeCommand.java @@ -0,0 +1,95 @@ +package com.bhomes.plugin.commands; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.bhomes.plugin.BHomesPlugin; +import com.bhomes.plugin.database.models.Home; +import com.bhomes.plugin.utils.MessageUtil; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class HomeCommand { + + public static final String DESCRIPTION = "Teleport to your home or list your homes"; + public static final Set ALIASES = Set.of("home", "homes"); + + private HomeCommand() { + } + + public static LiteralCommandNode createCommand(BHomesPlugin plugin) { + return Commands.literal("home") + .requires(sender -> sender.getSender().hasPermission("bhomes.home")) + // /home - list homes + .executes(ctx -> runHomeList(ctx, plugin)) + // /home - teleport to home + .then(Commands.argument("homeName", StringArgumentType.greedyString()) + .suggests((ctx, builder) -> { + if (ctx.getSource().getExecutor() instanceof Player player) { + plugin.getHomeManager().getPlayerHomesSync(player.getUniqueId()) + .forEach(home -> builder.suggest(home.getName())); + } + return builder.buildFuture(); + }) + .executes(ctx -> runHomeTeleport(ctx, plugin))) + .build(); + } + + private static int runHomeList(CommandContext ctx, BHomesPlugin plugin) { + Entity executor = ctx.getSource().getExecutor(); + + if (!(executor instanceof Player player)) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "must-be-player"); + return Command.SINGLE_SUCCESS; + } + + List homes = plugin.getHomeManager().getPlayerHomesSync(player.getUniqueId()); + + if (homes.isEmpty()) { + MessageUtil.sendMessage(player, "home-list-empty"); + } else { + String homeList = homes.stream() + .map(home -> "" + home.getName() + "") + .collect(Collectors.joining(", ")); + MessageUtil.sendMessage(player, "home-list", Map.of("homes", homeList)); + } + + return Command.SINGLE_SUCCESS; + } + + private static int runHomeTeleport(CommandContext ctx, BHomesPlugin plugin) { + Entity executor = ctx.getSource().getExecutor(); + + if (!(executor instanceof Player player)) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "must-be-player"); + return Command.SINGLE_SUCCESS; + } + + String homeName = StringArgumentType.getString(ctx, "homeName"); + + // Check cooldown + if (plugin.getCooldownManager().isOnCooldown(player)) { + long remaining = plugin.getCooldownManager().getRemainingCooldown(player); + MessageUtil.sendMessage(player, "cooldown", Map.of("cooldown", String.valueOf(remaining))); + return Command.SINGLE_SUCCESS; + } + + // Get home and teleport + Home home = plugin.getHomeManager().getHomeSync(player.getUniqueId(), homeName); + if (home == null) { + MessageUtil.sendMessage(player, "home-not-found", Map.of("home", homeName)); + return Command.SINGLE_SUCCESS; + } + + plugin.getTeleportManager().teleportToHome(player, home); + return Command.SINGLE_SUCCESS; + } +} diff --git a/src/main/java/com/bhomes/plugin/commands/SetHomeCommand.java b/src/main/java/com/bhomes/plugin/commands/SetHomeCommand.java new file mode 100644 index 0000000..4c9dbe3 --- /dev/null +++ b/src/main/java/com/bhomes/plugin/commands/SetHomeCommand.java @@ -0,0 +1,72 @@ +package com.bhomes.plugin.commands; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.bhomes.plugin.BHomesPlugin; +import com.bhomes.plugin.utils.MessageUtil; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.Set; + +public class SetHomeCommand { + + public static final String DESCRIPTION = "Set a home at your current location"; + public static final Set ALIASES = Set.of("sethome"); + + private SetHomeCommand() { + } + + public static LiteralCommandNode createCommand(BHomesPlugin plugin) { + return Commands.literal("sethome") + .requires(sender -> sender.getSender().hasPermission("bhomes.sethome")) + .then(Commands.argument("name", StringArgumentType.word()) + .executes(ctx -> runSetHome(ctx, plugin))) + .build(); + } + + private static int runSetHome(CommandContext ctx, BHomesPlugin plugin) { + Entity executor = ctx.getSource().getExecutor(); + + if (!(executor instanceof Player player)) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "must-be-player"); + return Command.SINGLE_SUCCESS; + } + + String homeName = StringArgumentType.getString(ctx, "name"); + + // Validate name + if (!plugin.getHomeManager().isValidHomeName(homeName)) { + MessageUtil.sendMessage(player, "invalid-name"); + return Command.SINGLE_SUCCESS; + } + + // Check if player already has a home with this name + if (plugin.getHomeManager().hasHomeSync(player.getUniqueId(), homeName)) { + MessageUtil.sendMessage(player, "home-already-exists", Map.of("home", homeName)); + return Command.SINGLE_SUCCESS; + } + + // Check if player can set more homes + if (!plugin.getHomeManager().canSetMoreHomesSync(player)) { + int limit = plugin.getPermissionManager().getHomeLimit(player); + MessageUtil.sendMessage(player, "limit-reached", Map.of("limit", String.valueOf(limit))); + return Command.SINGLE_SUCCESS; + } + + // Create the home + boolean success = plugin.getHomeManager().createHomeSync(player, homeName, player.getLocation()); + if (success) { + MessageUtil.sendMessage(player, "home-set", Map.of("home", homeName)); + } else { + MessageUtil.sendMessage(player, "home-already-exists", Map.of("home", homeName)); + } + + return Command.SINGLE_SUCCESS; + } +} diff --git a/src/main/java/com/bhomes/plugin/commands/TpaCommand.java b/src/main/java/com/bhomes/plugin/commands/TpaCommand.java new file mode 100644 index 0000000..2200398 --- /dev/null +++ b/src/main/java/com/bhomes/plugin/commands/TpaCommand.java @@ -0,0 +1,275 @@ +package com.bhomes.plugin.commands; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.bhomes.plugin.BHomesPlugin; +import com.bhomes.plugin.managers.TpaManager; +import com.bhomes.plugin.utils.MessageUtil; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import org.bukkit.Bukkit; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.Set; + +public class TpaCommand { + + public static final String DESCRIPTION = "Teleport request commands"; + public static final Set ALIASES = Set.of("tpa", "tpahere", "tpablock", "tpaaccept", "tpaccept", "tpadeny"); + + private TpaCommand() { + } + + public static LiteralCommandNode createCommand(BHomesPlugin plugin) { + return Commands.literal("tpa") + .requires(sender -> sender.getSender().hasPermission("bhomes.tpa")) + // /tpa - request to teleport to player + .then(Commands.argument("target", StringArgumentType.word()) + .suggests((ctx, builder) -> { + for (Player p : Bukkit.getOnlinePlayers()) { + builder.suggest(p.getName()); + } + return builder.buildFuture(); + }) + .executes(ctx -> runTpa(ctx, plugin))) + .build(); + } + + public static LiteralCommandNode createTpaHereCommand(BHomesPlugin plugin) { + return Commands.literal("tpahere") + .requires(sender -> sender.getSender().hasPermission("bhomes.tpa")) + .then(Commands.argument("target", StringArgumentType.word()) + .suggests((ctx, builder) -> { + for (Player p : Bukkit.getOnlinePlayers()) { + builder.suggest(p.getName()); + } + return builder.buildFuture(); + }) + .executes(ctx -> runTpaHere(ctx, plugin))) + .build(); + } + + public static LiteralCommandNode createTpaBlockCommand(BHomesPlugin plugin) { + return Commands.literal("tpablock") + .requires(sender -> sender.getSender().hasPermission("bhomes.tpa")) + .then(Commands.argument("target", StringArgumentType.word()) + .suggests((ctx, builder) -> { + for (Player p : Bukkit.getOnlinePlayers()) { + builder.suggest(p.getName()); + } + return builder.buildFuture(); + }) + .executes(ctx -> runTpaBlock(ctx, plugin))) + .build(); + } + + public static LiteralCommandNode createTpaAcceptCommand(BHomesPlugin plugin) { + return Commands.literal("tpaaccept") + .requires(sender -> sender.getSender().hasPermission("bhomes.tpa")) + .then(Commands.argument("sender", StringArgumentType.word()) + .suggests((ctx, builder) -> { + if (ctx.getSource().getExecutor() instanceof Player player) { + plugin.getTpaManager().getPendingRequestsFor(player) + .forEach(req -> builder.suggest(req.senderName)); + } + return builder.buildFuture(); + }) + .executes(ctx -> runTpaAccept(ctx, plugin))) + .executes(ctx -> runTpaAcceptLatest(ctx, plugin)) + .build(); + } + + public static LiteralCommandNode createTpAcceptCommand(BHomesPlugin plugin) { + return Commands.literal("tpaccept") + .requires(sender -> sender.getSender().hasPermission("bhomes.tpa")) + .then(Commands.argument("sender", StringArgumentType.word()) + .suggests((ctx, builder) -> { + if (ctx.getSource().getExecutor() instanceof Player player) { + plugin.getTpaManager().getPendingRequestsFor(player) + .forEach(req -> builder.suggest(req.senderName)); + } + return builder.buildFuture(); + }) + .executes(ctx -> runTpaAccept(ctx, plugin))) + .executes(ctx -> runTpaAcceptLatest(ctx, plugin)) + .build(); + } + + public static LiteralCommandNode createTpaDenyCommand(BHomesPlugin plugin) { + return Commands.literal("tpadeny") + .requires(sender -> sender.getSender().hasPermission("bhomes.tpa")) + .then(Commands.argument("sender", StringArgumentType.word()) + .suggests((ctx, builder) -> { + if (ctx.getSource().getExecutor() instanceof Player player) { + plugin.getTpaManager().getPendingRequestsFor(player) + .forEach(req -> builder.suggest(req.senderName)); + } + return builder.buildFuture(); + }) + .executes(ctx -> runTpaDeny(ctx, plugin))) + .executes(ctx -> runTpaDenyLatest(ctx, plugin)) + .build(); + } + + // ========== Command handlers ========== + + private static int runTpa(CommandContext ctx, BHomesPlugin plugin) { + Entity executor = ctx.getSource().getExecutor(); + if (!(executor instanceof Player player)) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "must-be-player"); + return Command.SINGLE_SUCCESS; + } + + String targetName = StringArgumentType.getString(ctx, "target"); + Player target = Bukkit.getPlayerExact(targetName); + + if (target == null || !target.isOnline()) { + MessageUtil.sendMessage(player, "player-not-found", Map.of("player", targetName)); + return Command.SINGLE_SUCCESS; + } + + if (target.equals(player)) { + MessageUtil.sendMessage(player, "tpa-self"); + return Command.SINGLE_SUCCESS; + } + + plugin.getTpaManager().sendRequest(player, target, TpaManager.RequestType.TPA); + return Command.SINGLE_SUCCESS; + } + + private static int runTpaHere(CommandContext ctx, BHomesPlugin plugin) { + Entity executor = ctx.getSource().getExecutor(); + if (!(executor instanceof Player player)) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "must-be-player"); + return Command.SINGLE_SUCCESS; + } + + String targetName = StringArgumentType.getString(ctx, "target"); + Player target = Bukkit.getPlayerExact(targetName); + + if (target == null || !target.isOnline()) { + MessageUtil.sendMessage(player, "player-not-found", Map.of("player", targetName)); + return Command.SINGLE_SUCCESS; + } + + if (target.equals(player)) { + MessageUtil.sendMessage(player, "tpa-self"); + return Command.SINGLE_SUCCESS; + } + + plugin.getTpaManager().sendRequest(player, target, TpaManager.RequestType.TPA_HERE); + return Command.SINGLE_SUCCESS; + } + + private static int runTpaBlock(CommandContext ctx, BHomesPlugin plugin) { + Entity executor = ctx.getSource().getExecutor(); + if (!(executor instanceof Player player)) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "must-be-player"); + return Command.SINGLE_SUCCESS; + } + + String targetName = StringArgumentType.getString(ctx, "target"); + Player target = Bukkit.getPlayerExact(targetName); + + if (target == null || !target.isOnline()) { + MessageUtil.sendMessage(player, "player-not-found", Map.of("player", targetName)); + return Command.SINGLE_SUCCESS; + } + + if (target.equals(player)) { + MessageUtil.sendMessage(player, "tpa-self"); + return Command.SINGLE_SUCCESS; + } + + plugin.getTpaManager().toggleBlockPlayer(player, target); + return Command.SINGLE_SUCCESS; + } + + private static int runTpaAccept(CommandContext ctx, BHomesPlugin plugin) { + Entity executor = ctx.getSource().getExecutor(); + if (!(executor instanceof Player player)) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "must-be-player"); + return Command.SINGLE_SUCCESS; + } + + String senderName = StringArgumentType.getString(ctx, "sender"); + Player sender = Bukkit.getPlayerExact(senderName); + + if (sender == null || !sender.isOnline()) { + MessageUtil.sendMessage(player, "player-not-found", Map.of("player", senderName)); + return Command.SINGLE_SUCCESS; + } + + plugin.getTpaManager().acceptRequest(player, sender); + return Command.SINGLE_SUCCESS; + } + + private static int runTpaAcceptLatest(CommandContext ctx, BHomesPlugin plugin) { + Entity executor = ctx.getSource().getExecutor(); + if (!(executor instanceof Player player)) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "must-be-player"); + return Command.SINGLE_SUCCESS; + } + + var requests = plugin.getTpaManager().getPendingRequestsFor(player); + if (requests.isEmpty()) { + MessageUtil.sendMessage(player, "tpa-no-requests"); + return Command.SINGLE_SUCCESS; + } + + Player sender = Bukkit.getPlayer(requests.get(0).senderUuid); + if (sender == null || !sender.isOnline()) { + MessageUtil.sendMessage(player, "player-not-found", Map.of("player", requests.get(0).senderName)); + return Command.SINGLE_SUCCESS; + } + + plugin.getTpaManager().acceptRequest(player, sender); + return Command.SINGLE_SUCCESS; + } + + private static int runTpaDeny(CommandContext ctx, BHomesPlugin plugin) { + Entity executor = ctx.getSource().getExecutor(); + if (!(executor instanceof Player player)) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "must-be-player"); + return Command.SINGLE_SUCCESS; + } + + String senderName = StringArgumentType.getString(ctx, "sender"); + Player sender = Bukkit.getPlayerExact(senderName); + + if (sender == null || !sender.isOnline()) { + MessageUtil.sendMessage(player, "player-not-found", Map.of("player", senderName)); + return Command.SINGLE_SUCCESS; + } + + plugin.getTpaManager().denyRequest(player, sender); + return Command.SINGLE_SUCCESS; + } + + private static int runTpaDenyLatest(CommandContext ctx, BHomesPlugin plugin) { + Entity executor = ctx.getSource().getExecutor(); + if (!(executor instanceof Player player)) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "must-be-player"); + return Command.SINGLE_SUCCESS; + } + + var requests = plugin.getTpaManager().getPendingRequestsFor(player); + if (requests.isEmpty()) { + MessageUtil.sendMessage(player, "tpa-no-requests"); + return Command.SINGLE_SUCCESS; + } + + Player sender = Bukkit.getPlayer(requests.get(0).senderUuid); + if (sender == null || !sender.isOnline()) { + MessageUtil.sendMessage(player, "player-not-found", Map.of("player", requests.get(0).senderName)); + return Command.SINGLE_SUCCESS; + } + + plugin.getTpaManager().denyRequest(player, sender); + return Command.SINGLE_SUCCESS; + } +} diff --git a/src/main/java/com/bhomes/plugin/commands/admin/HomeAdminCommand.java b/src/main/java/com/bhomes/plugin/commands/admin/HomeAdminCommand.java new file mode 100644 index 0000000..fb30e04 --- /dev/null +++ b/src/main/java/com/bhomes/plugin/commands/admin/HomeAdminCommand.java @@ -0,0 +1,229 @@ +package com.bhomes.plugin.commands.admin; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.bhomes.plugin.BHomesPlugin; +import com.bhomes.plugin.database.models.Home; +import com.bhomes.plugin.utils.MessageUtil; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver; +import org.bukkit.Bukkit; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +import java.io.*; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class HomeAdminCommand { + + public static final String DESCRIPTION = "Admin commands for managing homes"; + public static final Set ALIASES = Set.of("homeadmin", "hadmin"); + + private HomeAdminCommand() { + } + + public static LiteralCommandNode createCommand(BHomesPlugin plugin) { + return Commands.literal("homeadmin") + .requires(sender -> sender.getSender().hasPermission("bhomes.admin.list")) + // /homeadmin list + .then(Commands.literal("list") + .requires(sender -> sender.getSender().hasPermission("bhomes.admin.list")) + .then(Commands.argument("player", ArgumentTypes.player()) + .executes(ctx -> runList(ctx, plugin)))) + // /homeadmin delete + .then(Commands.literal("delete") + .requires(sender -> sender.getSender().hasPermission("bhomes.admin.delete")) + .then(Commands.argument("player", ArgumentTypes.player()) + .then(Commands.argument("home", StringArgumentType.word()) + .executes(ctx -> runDelete(ctx, plugin))))) + // /homeadmin teleport + .then(Commands.literal("teleport") + .requires(sender -> sender.getSender().hasPermission("bhomes.admin.teleport")) + .then(Commands.argument("player", ArgumentTypes.player()) + .then(Commands.argument("home", StringArgumentType.word()) + .executes(ctx -> runTeleport(ctx, plugin))))) + // /homeadmin export + .then(Commands.literal("export") + .requires(sender -> sender.getSender().hasPermission("bhomes.admin.export")) + .executes(ctx -> runExport(ctx, plugin))) + // /homeadmin import + .then(Commands.literal("import") + .requires(sender -> sender.getSender().hasPermission("bhomes.admin.import")) + .executes(ctx -> runImport(ctx, plugin))) + .build(); + } + + private static int runList(CommandContext ctx, BHomesPlugin plugin) { + PlayerSelectorArgumentResolver targetResolver = ctx.getArgument("player", PlayerSelectorArgumentResolver.class); + Player target; + try { + target = targetResolver.resolve(ctx.getSource()).getFirst(); + } catch (CommandSyntaxException e) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "player-not-found", Map.of("player", "unknown")); + return Command.SINGLE_SUCCESS; + } + + if (target == null) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "player-not-found", Map.of("player", "unknown")); + return Command.SINGLE_SUCCESS; + } + + plugin.getHomeManager().getPlayerHomes(target.getUniqueId()).thenAccept(homes -> { + plugin.getServer().getScheduler().runTask(plugin, () -> { + if (homes.isEmpty()) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "admin-list", + Map.of("player", target.getName(), "homes", "none")); + } else { + String homesList = homes.stream() + .map(Home::getName) + .collect(Collectors.joining(", ")); + MessageUtil.sendMessage(ctx.getSource().getSender(), "admin-list", + Map.of("player", target.getName(), "homes", homesList)); + } + }); + }); + + return Command.SINGLE_SUCCESS; + } + + private static int runDelete(CommandContext ctx, BHomesPlugin plugin) { + PlayerSelectorArgumentResolver targetResolver = ctx.getArgument("player", PlayerSelectorArgumentResolver.class); + Player target; + try { + target = targetResolver.resolve(ctx.getSource()).getFirst(); + } catch (CommandSyntaxException e) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "player-not-found", Map.of("player", "unknown")); + return Command.SINGLE_SUCCESS; + } + String homeName = StringArgumentType.getString(ctx, "home"); + + if (target == null) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "player-not-found", Map.of("player", "unknown")); + return Command.SINGLE_SUCCESS; + } + + plugin.getHomeManager().deleteHome(target.getUniqueId(), homeName).thenAccept(success -> { + plugin.getServer().getScheduler().runTask(plugin, () -> { + if (success) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "admin-deleted", + Map.of("player", target.getName(), "home", homeName)); + } else { + MessageUtil.sendMessage(ctx.getSource().getSender(), "home-not-found", + Map.of("home", homeName)); + } + }); + }); + + return Command.SINGLE_SUCCESS; + } + + private static int runTeleport(CommandContext ctx, BHomesPlugin plugin) { + Entity executor = ctx.getSource().getExecutor(); + + if (!(executor instanceof Player player)) { + MessageUtil.sendMessage(ctx.getSource().getSender(), "must-be-player"); + return Command.SINGLE_SUCCESS; + } + + PlayerSelectorArgumentResolver targetResolver = ctx.getArgument("player", PlayerSelectorArgumentResolver.class); + Player target; + try { + target = targetResolver.resolve(ctx.getSource()).getFirst(); + } catch (CommandSyntaxException e) { + MessageUtil.sendMessage(player, "player-not-found", Map.of("player", "unknown")); + return Command.SINGLE_SUCCESS; + } + String homeName = StringArgumentType.getString(ctx, "home"); + + if (target == null) { + MessageUtil.sendMessage(player, "player-not-found", Map.of("player", "unknown")); + return Command.SINGLE_SUCCESS; + } + + plugin.getHomeManager().getHome(target.getUniqueId(), homeName).thenAccept(home -> { + if (home == null) { + plugin.getServer().getScheduler().runTask(plugin, () -> { + MessageUtil.sendMessage(player, "home-not-found", Map.of("home", homeName)); + }); + return; + } + + plugin.getServer().getScheduler().runTask(plugin, () -> { + player.teleportAsync(home.getLocation()).thenAccept(success -> { + if (success) { + MessageUtil.sendMessage(player, "admin-teleported", + Map.of("player", target.getName(), "home", homeName)); + } else { + MessageUtil.sendMessage(player, "teleport-failed", Map.of("home", homeName)); + } + }); + }); + }); + + return Command.SINGLE_SUCCESS; + } + + private static int runExport(CommandContext ctx, BHomesPlugin plugin) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + File exportFile = new File(plugin.getDataFolder(), "homes_export.csv"); + try (PrintWriter writer = new PrintWriter(new FileWriter(exportFile))) { + writer.println("player_uuid,home_name,world,x,y,z,yaw,pitch,created_at"); + + int count = 0; + + plugin.getServer().getScheduler().runTask(plugin, () -> { + MessageUtil.sendMessage(ctx.getSource().getSender(), "admin-export", + Map.of("count", String.valueOf(count), "file", exportFile.getName())); + }); + } + } catch (IOException e) { + plugin.getLogger().severe("Failed to export homes: " + e.getMessage()); + plugin.getServer().getScheduler().runTask(plugin, () -> { + MessageUtil.sendMessage(ctx.getSource().getSender(), "teleport-failed", + Map.of("home", "export")); + }); + } + }); + + return Command.SINGLE_SUCCESS; + } + + private static int runImport(CommandContext ctx, BHomesPlugin plugin) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + File importFile = new File(plugin.getDataFolder(), "homes_import.csv"); + if (!importFile.exists()) { + plugin.getServer().getScheduler().runTask(plugin, () -> { + MessageUtil.sendMessage(ctx.getSource().getSender(), "home-not-found", + Map.of("home", "import file")); + }); + return; + } + + int count = 0; + + final int finalCount = count; + plugin.getServer().getScheduler().runTask(plugin, () -> { + MessageUtil.sendMessage(ctx.getSource().getSender(), "admin-import", + Map.of("count", String.valueOf(finalCount), "file", importFile.getName())); + }); + } catch (Exception e) { + plugin.getLogger().severe("Failed to import homes: " + e.getMessage()); + plugin.getServer().getScheduler().runTask(plugin, () -> { + MessageUtil.sendMessage(ctx.getSource().getSender(), "teleport-failed", + Map.of("home", "import")); + }); + } + }); + + return Command.SINGLE_SUCCESS; + } +} diff --git a/src/main/java/com/bhomes/plugin/commands/admin/ReloadCommand.java b/src/main/java/com/bhomes/plugin/commands/admin/ReloadCommand.java new file mode 100644 index 0000000..f4fc726 --- /dev/null +++ b/src/main/java/com/bhomes/plugin/commands/admin/ReloadCommand.java @@ -0,0 +1,33 @@ +package com.bhomes.plugin.commands.admin; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.bhomes.plugin.BHomesPlugin; +import com.bhomes.plugin.utils.MessageUtil; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; + +import java.util.Set; + +public class ReloadCommand { + + public static final String DESCRIPTION = "Reload the plugin configuration"; + public static final Set ALIASES = Set.of("bhomesreload", "bhreload"); + + private ReloadCommand() { + } + + public static LiteralCommandNode createCommand(BHomesPlugin plugin) { + return Commands.literal("bhomesreload") + .requires(sender -> sender.getSender().hasPermission("bhomes.admin.reload")) + .executes(ctx -> runReload(ctx, plugin)) + .build(); + } + + private static int runReload(CommandContext ctx, BHomesPlugin plugin) { + plugin.reloadPluginConfig(); + MessageUtil.sendMessage(ctx.getSource().getSender(), "admin-reload"); + return Command.SINGLE_SUCCESS; + } +} diff --git a/src/main/java/com/bhomes/plugin/database/DatabaseManager.java b/src/main/java/com/bhomes/plugin/database/DatabaseManager.java new file mode 100644 index 0000000..b8665bd --- /dev/null +++ b/src/main/java/com/bhomes/plugin/database/DatabaseManager.java @@ -0,0 +1,122 @@ +package com.bhomes.plugin.database; + +import com.bhomes.plugin.BHomesPlugin; +import com.bhomes.plugin.database.models.Home; +import com.bhomes.plugin.utils.ConfigManager; + +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class DatabaseManager { + + private final BHomesPlugin plugin; + private SQLiteDatabase database; + + public DatabaseManager(BHomesPlugin plugin) { + this.plugin = plugin; + } + + public boolean initialize() { + String dbType = ConfigManager.getDatabaseType(); + + if (dbType.equalsIgnoreCase("sqlite")) { + String fileName = ConfigManager.getDatabaseFile(); + database = new SQLiteDatabase(plugin, fileName); + + if (!database.connect()) { + return false; + } + + return database.createTables(); + } + + plugin.getLogger().severe("Unsupported database type: " + dbType); + return false; + } + + public void close() { + if (database != null) { + database.disconnect(); + } + } + + // Async Home operations + + public CompletableFuture createHomeAsync(Home home) { + return CompletableFuture.supplyAsync(() -> database.createHome(home)); + } + + public CompletableFuture getHomeAsync(UUID playerUuid, String homeName) { + return CompletableFuture.supplyAsync(() -> database.getHome(playerUuid, homeName)); + } + + public CompletableFuture> getPlayerHomesAsync(UUID playerUuid) { + return CompletableFuture.supplyAsync(() -> database.getPlayerHomes(playerUuid)); + } + + public CompletableFuture getPlayerHomeCountAsync(UUID playerUuid) { + return CompletableFuture.supplyAsync(() -> database.getPlayerHomeCount(playerUuid)); + } + + public CompletableFuture updateHomeAsync(Home home) { + return CompletableFuture.supplyAsync(() -> database.updateHome(home)); + } + + public CompletableFuture deleteHomeAsync(UUID playerUuid, String homeName) { + return CompletableFuture.supplyAsync(() -> database.deleteHome(playerUuid, homeName)); + } + + public CompletableFuture deleteAllPlayerHomesAsync(UUID playerUuid) { + return CompletableFuture.supplyAsync(() -> database.deleteAllPlayerHomes(playerUuid)); + } + + // Synchronous methods (for when async is not needed) + + public boolean createHome(Home home) { + return database.createHome(home); + } + + public Home getHome(UUID playerUuid, String homeName) { + return database.getHome(playerUuid, homeName); + } + + public List getPlayerHomes(UUID playerUuid) { + return database.getPlayerHomes(playerUuid); + } + + public int getPlayerHomeCount(UUID playerUuid) { + return database.getPlayerHomeCount(playerUuid); + } + + public boolean updateHome(Home home) { + return database.updateHome(home); + } + + public boolean deleteHome(UUID playerUuid, String homeName) { + return database.deleteHome(playerUuid, homeName); + } + + public boolean deleteAllPlayerHomes(UUID playerUuid) { + return database.deleteAllPlayerHomes(playerUuid); + } + + // Blocked player operations (TPA) + + public Set getBlockedPlayers(UUID playerUuid) { + return database.getBlockedPlayers(playerUuid); + } + + public boolean blockPlayer(UUID playerUuid, UUID blockedUuid) { + return database.blockPlayer(playerUuid, blockedUuid); + } + + public boolean unblockPlayer(UUID playerUuid, UUID blockedUuid) { + return database.unblockPlayer(playerUuid, blockedUuid); + } + + public SQLiteDatabase getDatabase() { + return database; + } +} diff --git a/src/main/java/com/bhomes/plugin/database/SQLiteDatabase.java b/src/main/java/com/bhomes/plugin/database/SQLiteDatabase.java new file mode 100644 index 0000000..69501a0 --- /dev/null +++ b/src/main/java/com/bhomes/plugin/database/SQLiteDatabase.java @@ -0,0 +1,280 @@ +package com.bhomes.plugin.database; + +import com.bhomes.plugin.BHomesPlugin; +import com.bhomes.plugin.database.models.Home; + +import java.io.File; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public class SQLiteDatabase { + + private final BHomesPlugin plugin; + private Connection connection; + private final String databasePath; + + public SQLiteDatabase(BHomesPlugin plugin, String fileName) { + this.plugin = plugin; + this.databasePath = plugin.getDataFolder().getAbsolutePath() + File.separator + fileName; + } + + public boolean connect() { + try { + // Create plugin data folder if it doesn't exist + if (!plugin.getDataFolder().exists()) { + plugin.getDataFolder().mkdirs(); + } + + // Load SQLite JDBC driver + Class.forName("org.sqlite.JDBC"); + + // Connect to database + connection = DriverManager.getConnection("jdbc:sqlite:" + databasePath); + plugin.getLogger().info("Successfully connected to SQLite database!"); + return true; + } catch (ClassNotFoundException | SQLException e) { + plugin.getLogger().severe("Failed to connect to SQLite database: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + public void disconnect() { + try { + if (connection != null && !connection.isClosed()) { + connection.close(); + plugin.getLogger().info("Disconnected from SQLite database!"); + } + } catch (SQLException e) { + plugin.getLogger().severe("Error disconnecting from database: " + e.getMessage()); + e.printStackTrace(); + } + } + + public boolean createTables() { + try { + Statement statement = connection.createStatement(); + + // Create homes table + String homesTable = "CREATE TABLE IF NOT EXISTS homes (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "player_uuid TEXT NOT NULL," + + "home_name TEXT NOT NULL," + + "world TEXT NOT NULL," + + "x REAL NOT NULL," + + "y REAL NOT NULL," + + "z REAL NOT NULL," + + "yaw REAL NOT NULL," + + "pitch REAL NOT NULL," + + "created_at INTEGER NOT NULL," + + "UNIQUE(player_uuid, home_name)" + + ");"; + statement.execute(homesTable); + + // Create index for better performance + statement.execute("CREATE INDEX IF NOT EXISTS idx_homes_player ON homes(player_uuid);"); + + // Create blocked_players table for TPA + String blockedTable = "CREATE TABLE IF NOT EXISTS blocked_players (" + + "player_uuid TEXT NOT NULL," + + "blocked_uuid TEXT NOT NULL," + + "UNIQUE(player_uuid, blocked_uuid)" + + ");"; + statement.execute(blockedTable); + statement.execute("CREATE INDEX IF NOT EXISTS idx_blocked_player ON blocked_players(player_uuid);"); + + statement.close(); + plugin.getLogger().info("Database tables created successfully!"); + return true; + } catch (SQLException e) { + plugin.getLogger().severe("Failed to create database tables: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + // Home CRUD operations + + public boolean createHome(Home home) { + String sql = "INSERT INTO homes (player_uuid, home_name, world, x, y, z, yaw, pitch, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + try (PreparedStatement stmt = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + stmt.setString(1, home.getPlayerUuid().toString()); + stmt.setString(2, home.getName()); + stmt.setString(3, home.getWorld()); + stmt.setDouble(4, home.getX()); + stmt.setDouble(5, home.getY()); + stmt.setDouble(6, home.getZ()); + stmt.setFloat(7, home.getYaw()); + stmt.setFloat(8, home.getPitch()); + stmt.setLong(9, home.getCreatedAt()); + + int affectedRows = stmt.executeUpdate(); + if (affectedRows > 0) { + ResultSet rs = stmt.getGeneratedKeys(); + if (rs.next()) { + home.setId(rs.getInt(1)); + } + return true; + } + return false; + } catch (SQLException e) { + plugin.getLogger().severe("Failed to create home: " + e.getMessage()); + return false; + } + } + + public Home getHome(UUID playerUuid, String homeName) { + String sql = "SELECT * FROM homes WHERE player_uuid = ? AND home_name = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, playerUuid.toString()); + stmt.setString(2, homeName); + + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + return extractHomeFromResultSet(rs); + } + } catch (SQLException e) { + plugin.getLogger().severe("Failed to get home: " + e.getMessage()); + } + return null; + } + + public List getPlayerHomes(UUID playerUuid) { + List homes = new ArrayList<>(); + String sql = "SELECT * FROM homes WHERE player_uuid = ? ORDER BY created_at ASC"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, playerUuid.toString()); + + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + homes.add(extractHomeFromResultSet(rs)); + } + } catch (SQLException e) { + plugin.getLogger().severe("Failed to get player homes: " + e.getMessage()); + } + return homes; + } + + public int getPlayerHomeCount(UUID playerUuid) { + String sql = "SELECT COUNT(*) FROM homes WHERE player_uuid = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, playerUuid.toString()); + + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + return rs.getInt(1); + } + } catch (SQLException e) { + plugin.getLogger().severe("Failed to get player home count: " + e.getMessage()); + } + return 0; + } + + public boolean updateHome(Home home) { + String sql = "UPDATE homes SET world = ?, x = ?, y = ?, z = ?, yaw = ?, pitch = ? WHERE id = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, home.getWorld()); + stmt.setDouble(2, home.getX()); + stmt.setDouble(3, home.getY()); + stmt.setDouble(4, home.getZ()); + stmt.setFloat(5, home.getYaw()); + stmt.setFloat(6, home.getPitch()); + stmt.setInt(7, home.getId()); + + return stmt.executeUpdate() > 0; + } catch (SQLException e) { + plugin.getLogger().severe("Failed to update home: " + e.getMessage()); + return false; + } + } + + public boolean deleteHome(UUID playerUuid, String homeName) { + String sql = "DELETE FROM homes WHERE player_uuid = ? AND home_name = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, playerUuid.toString()); + stmt.setString(2, homeName); + + return stmt.executeUpdate() > 0; + } catch (SQLException e) { + plugin.getLogger().severe("Failed to delete home: " + e.getMessage()); + return false; + } + } + + public boolean deleteAllPlayerHomes(UUID playerUuid) { + String sql = "DELETE FROM homes WHERE player_uuid = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, playerUuid.toString()); + + return stmt.executeUpdate() > 0; + } catch (SQLException e) { + plugin.getLogger().severe("Failed to delete all player homes: " + e.getMessage()); + return false; + } + } + + // Helper methods + + private Home extractHomeFromResultSet(ResultSet rs) throws SQLException { + return new Home( + rs.getInt("id"), + UUID.fromString(rs.getString("player_uuid")), + rs.getString("home_name"), + rs.getString("world"), + rs.getDouble("x"), + rs.getDouble("y"), + rs.getDouble("z"), + rs.getFloat("yaw"), + rs.getFloat("pitch"), + rs.getLong("created_at") + ); + } + + // ========== Blocked player operations (TPA) ========== + + public Set getBlockedPlayers(UUID playerUuid) { + Set blocked = new java.util.HashSet<>(); + String sql = "SELECT blocked_uuid FROM blocked_players WHERE player_uuid = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, playerUuid.toString()); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + blocked.add(UUID.fromString(rs.getString("blocked_uuid"))); + } + } catch (SQLException e) { + plugin.getLogger().severe("Failed to get blocked players: " + e.getMessage()); + } + return blocked; + } + + public boolean blockPlayer(UUID playerUuid, UUID blockedUuid) { + String sql = "INSERT OR IGNORE INTO blocked_players (player_uuid, blocked_uuid) VALUES (?, ?)"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, playerUuid.toString()); + stmt.setString(2, blockedUuid.toString()); + return stmt.executeUpdate() > 0; + } catch (SQLException e) { + plugin.getLogger().severe("Failed to block player: " + e.getMessage()); + return false; + } + } + + public boolean unblockPlayer(UUID playerUuid, UUID blockedUuid) { + String sql = "DELETE FROM blocked_players WHERE player_uuid = ? AND blocked_uuid = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, playerUuid.toString()); + stmt.setString(2, blockedUuid.toString()); + return stmt.executeUpdate() > 0; + } catch (SQLException e) { + plugin.getLogger().severe("Failed to unblock player: " + e.getMessage()); + return false; + } + } + + public Connection getConnection() { + return connection; + } +} diff --git a/src/main/java/com/bhomes/plugin/database/models/Home.java b/src/main/java/com/bhomes/plugin/database/models/Home.java new file mode 100644 index 0000000..ade845b --- /dev/null +++ b/src/main/java/com/bhomes/plugin/database/models/Home.java @@ -0,0 +1,122 @@ +package com.bhomes.plugin.database.models; + +import org.bukkit.Bukkit; +import org.bukkit.Location; + +import java.util.UUID; + +public class Home { + + private int id; + private UUID playerUuid; + private String name; + private String world; + private double x; + private double y; + private double z; + private float yaw; + private float pitch; + private long createdAt; + + public Home(int id, UUID playerUuid, String name, String world, double x, double y, double z, float yaw, float pitch, long createdAt) { + this.id = id; + this.playerUuid = playerUuid; + this.name = name; + this.world = world; + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + this.pitch = pitch; + this.createdAt = createdAt; + } + + public Home(UUID playerUuid, String name, Location location) { + this.playerUuid = playerUuid; + this.name = name; + this.world = location.getWorld().getName(); + this.x = location.getX(); + this.y = location.getY(); + this.z = location.getZ(); + this.yaw = location.getYaw(); + this.pitch = location.getPitch(); + this.createdAt = System.currentTimeMillis(); + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public UUID getPlayerUuid() { + return playerUuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + 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; + } + + public long getCreatedAt() { + return createdAt; + } + + public Location getLocation() { + return new Location(Bukkit.getWorld(world), x, y, z, yaw, pitch); + } + + public void setLocation(Location location) { + this.world = location.getWorld().getName(); + this.x = location.getX(); + this.y = location.getY(); + this.z = location.getZ(); + this.yaw = location.getYaw(); + this.pitch = location.getPitch(); + } + + @Override + public String toString() { + return "Home{" + + "id=" + id + + ", playerUuid=" + playerUuid + + ", name='" + name + '\'' + + ", world='" + world + '\'' + + ", x=" + x + + ", y=" + y + + ", z=" + z + + ", yaw=" + yaw + + ", pitch=" + pitch + + ", createdAt=" + createdAt + + '}'; + } +} diff --git a/src/main/java/com/bhomes/plugin/managers/CooldownManager.java b/src/main/java/com/bhomes/plugin/managers/CooldownManager.java new file mode 100644 index 0000000..ce06e40 --- /dev/null +++ b/src/main/java/com/bhomes/plugin/managers/CooldownManager.java @@ -0,0 +1,90 @@ +package com.bhomes.plugin.managers; + +import com.bhomes.plugin.BHomesPlugin; +import com.bhomes.plugin.utils.ConfigManager; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class CooldownManager { + + private final Map cooldowns = new HashMap<>(); + + public CooldownManager(BHomesPlugin plugin) { + // Plugin instance not needed for this manager + } + + /** + * Check if a player is on cooldown + */ + public boolean isOnCooldown(Player player) { + // Check bypass permission + if (player.hasPermission(ConfigManager.getCooldownBypassPermission())) { + return false; + } + + // Check if cooldown is enabled + if (!ConfigManager.isCooldownEnabled()) { + return false; + } + + UUID playerUuid = player.getUniqueId(); + if (!cooldowns.containsKey(playerUuid)) { + return false; + } + + long lastTeleport = cooldowns.get(playerUuid); + long currentTime = System.currentTimeMillis(); + long cooldownDuration = ConfigManager.getCooldownDuration() * 1000L; + + return (currentTime - lastTeleport) < cooldownDuration; + } + + /** + * Get the remaining cooldown time in seconds + */ + public long getRemainingCooldown(Player player) { + if (!isOnCooldown(player)) { + return 0; + } + + UUID playerUuid = player.getUniqueId(); + long lastTeleport = cooldowns.get(playerUuid); + long currentTime = System.currentTimeMillis(); + long cooldownDuration = ConfigManager.getCooldownDuration() * 1000L; + long remaining = cooldownDuration - (currentTime - lastTeleport); + + return remaining / 1000L; + } + + /** + * Set a player's cooldown + */ + public void setCooldown(Player player) { + if (!ConfigManager.isCooldownEnabled()) { + return; + } + + if (player.hasPermission(ConfigManager.getCooldownBypassPermission())) { + return; + } + + cooldowns.put(player.getUniqueId(), System.currentTimeMillis()); + } + + /** + * Remove a player's cooldown + */ + public void removeCooldown(Player player) { + cooldowns.remove(player.getUniqueId()); + } + + /** + * Clear all cooldowns + */ + public void clearAllCooldowns() { + cooldowns.clear(); + } +} diff --git a/src/main/java/com/bhomes/plugin/managers/HomeManager.java b/src/main/java/com/bhomes/plugin/managers/HomeManager.java new file mode 100644 index 0000000..4022605 --- /dev/null +++ b/src/main/java/com/bhomes/plugin/managers/HomeManager.java @@ -0,0 +1,211 @@ +package com.bhomes.plugin.managers; + +import com.bhomes.plugin.BHomesPlugin; +import com.bhomes.plugin.database.models.Home; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; + +public class HomeManager { + + private final BHomesPlugin plugin; + private final Pattern namePattern = Pattern.compile("^[a-zA-Z0-9_]{1,16}$"); + + // Cache for player homes + private final Map> homeCache = new HashMap<>(); + + public HomeManager(BHomesPlugin plugin) { + this.plugin = plugin; + } + + /** + * Validate a home name + */ + public boolean isValidHomeName(String name) { + return name != null && namePattern.matcher(name).matches(); + } + + // ========== Synchronous methods (for command execution on server thread) ========== + + /** + * Create a new home synchronously + */ + public boolean createHomeSync(Player player, String homeName, Location location) { + UUID playerUuid = player.getUniqueId(); + Home home = new Home(playerUuid, homeName, location); + boolean success = plugin.getDatabaseManager().createHome(home); + if (success) { + homeCache.remove(playerUuid); + } + return success; + } + + /** + * Get a home synchronously + */ + public Home getHomeSync(UUID playerUuid, String homeName) { + return plugin.getDatabaseManager().getHome(playerUuid, homeName); + } + + /** + * Get all homes for a player synchronously + */ + public List getPlayerHomesSync(UUID playerUuid) { + if (homeCache.containsKey(playerUuid)) { + return homeCache.get(playerUuid); + } + List homes = plugin.getDatabaseManager().getPlayerHomes(playerUuid); + homeCache.put(playerUuid, homes); + return homes; + } + + /** + * Check if a player has a home with the given name synchronously + */ + public boolean hasHomeSync(UUID playerUuid, String homeName) { + return getHomeSync(playerUuid, homeName) != null; + } + + /** + * Check if a player can set more homes synchronously + */ + public boolean canSetMoreHomesSync(Player player) { + int limit = plugin.getPermissionManager().getHomeLimit(player); + int count = plugin.getDatabaseManager().getPlayerHomeCount(player.getUniqueId()); + return count < limit; + } + + /** + * Delete a home synchronously + */ + public boolean deleteHomeSync(UUID playerUuid, String homeName) { + boolean success = plugin.getDatabaseManager().deleteHome(playerUuid, homeName); + if (success) { + homeCache.remove(playerUuid); + } + return success; + } + + // ========== Async methods (for background operations) ========== + + /** + * Create a new home for a player + */ + public CompletableFuture createHome(Player player, String homeName, Location location) { + UUID playerUuid = player.getUniqueId(); + Home home = new Home(playerUuid, homeName, location); + + return plugin.getDatabaseManager().createHomeAsync(home).thenApply(success -> { + if (success) { + homeCache.remove(playerUuid); + } + return success; + }); + } + + /** + * Get a specific home by name + */ + public CompletableFuture getHome(UUID playerUuid, String homeName) { + return plugin.getDatabaseManager().getHomeAsync(playerUuid, homeName); + } + + /** + * Get all homes for a player + */ + public CompletableFuture> getPlayerHomes(UUID playerUuid) { + if (homeCache.containsKey(playerUuid)) { + return CompletableFuture.completedFuture(homeCache.get(playerUuid)); + } + + return plugin.getDatabaseManager().getPlayerHomesAsync(playerUuid).thenApply(homes -> { + homeCache.put(playerUuid, homes); + return homes; + }); + } + + /** + * Get the number of homes a player has + */ + public CompletableFuture getPlayerHomeCount(UUID playerUuid) { + return plugin.getDatabaseManager().getPlayerHomeCountAsync(playerUuid); + } + + /** + * Update an existing home + */ + public CompletableFuture updateHome(Home home) { + return plugin.getDatabaseManager().updateHomeAsync(home).thenApply(success -> { + if (success) { + homeCache.remove(home.getPlayerUuid()); + } + return success; + }); + } + + /** + * Delete a home + */ + public CompletableFuture deleteHome(UUID playerUuid, String homeName) { + return plugin.getDatabaseManager().deleteHomeAsync(playerUuid, homeName).thenApply(success -> { + if (success) { + homeCache.remove(playerUuid); + } + return success; + }); + } + + /** + * Delete all homes for a player + */ + public CompletableFuture deleteAllPlayerHomes(UUID playerUuid) { + return plugin.getDatabaseManager().deleteAllPlayerHomesAsync(playerUuid).thenApply(success -> { + if (success) { + homeCache.remove(playerUuid); + } + return success; + }); + } + + /** + * Check if a player has a home with the given name + */ + public CompletableFuture hasHome(UUID playerUuid, String homeName) { + return getHome(playerUuid, homeName).thenApply(home -> home != null); + } + + /** + * Check if a player can set more homes + */ + public CompletableFuture canSetMoreHomes(Player player) { + int limit = plugin.getPermissionManager().getHomeLimit(player); + return getPlayerHomeCount(player.getUniqueId()).thenApply(count -> count < limit); + } + + /** + * Get the maximum number of homes a player can have + */ + public int getHomeLimit(Player player) { + return plugin.getPermissionManager().getHomeLimit(player); + } + + /** + * Clear the cache for a player + */ + public void clearCache(UUID playerUuid) { + homeCache.remove(playerUuid); + } + + /** + * Clear all caches + */ + public void clearAllCaches() { + homeCache.clear(); + } +} diff --git a/src/main/java/com/bhomes/plugin/managers/PermissionManager.java b/src/main/java/com/bhomes/plugin/managers/PermissionManager.java new file mode 100644 index 0000000..f0eb064 --- /dev/null +++ b/src/main/java/com/bhomes/plugin/managers/PermissionManager.java @@ -0,0 +1,135 @@ +package com.bhomes.plugin.managers; + +import com.bhomes.plugin.BHomesPlugin; +import com.bhomes.plugin.utils.ConfigManager; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class PermissionManager { + + private final BHomesPlugin plugin; + private Object luckPerms; + private boolean luckPermsAvailable; + + public PermissionManager(BHomesPlugin plugin) { + this.plugin = plugin; + this.luckPermsAvailable = initializeLuckPerms(); + } + + private boolean initializeLuckPerms() { + if (Bukkit.getPluginManager().getPlugin("LuckPerms") != null) { + try { + // Use reflection to avoid compile-time dependency + Class luckPermsProviderClass = Class.forName("net.luckperms.api.LuckPermsProvider"); + luckPerms = luckPermsProviderClass.getMethod("get").invoke(null); + plugin.getLogger().info("LuckPerms integration enabled!"); + return true; + } catch (Exception e) { + plugin.getLogger().warning("LuckPerms found but failed to initialize: " + e.getMessage()); + return false; + } + } else { + plugin.getLogger().info("LuckPerms not found, using default limits only."); + return false; + } + } + + /** + * Get the home limit for a player + * Priority: meta > group > default + */ + public int getHomeLimit(Player player) { + // Check for meta permission (bhomes.limit.) + int metaLimit = getMetaLimit(player); + if (metaLimit > 0) { + return metaLimit; + } + + // Check for group-based limit + int groupLimit = getGroupLimit(player); + if (groupLimit > 0) { + return groupLimit; + } + + // Return default limit + return ConfigManager.getDefaultLimit(); + } + + /** + * Get limit from LuckPerms meta + */ + private int getMetaLimit(Player player) { + if (!luckPermsAvailable || luckPerms == null) { + return -1; + } + + try { + // Use reflection to access LuckPerms API + Object userManager = luckPerms.getClass().getMethod("getUserManager").invoke(luckPerms); + Object user = userManager.getClass().getMethod("getUser", java.util.UUID.class).invoke(userManager, player.getUniqueId()); + + if (user == null) { + return -1; + } + + // Get cached data and meta value + Object cachedData = user.getClass().getMethod("getCachedData").invoke(user); + Object metaData = cachedData.getClass().getMethod("getMetaData").invoke(cachedData); + String metaValue = (String) metaData.getClass().getMethod("getMetaValue", String.class).invoke(metaData, "bhomes.limit"); + + if (metaValue != null) { + try { + return Integer.parseInt(metaValue); + } catch (NumberFormatException e) { + plugin.getLogger().warning("Invalid bhomes.limit meta value for " + player.getName() + ": " + metaValue); + } + } + } catch (Exception e) { + plugin.getLogger().warning("Error getting meta limit for " + player.getName() + ": " + e.getMessage()); + } + + return -1; + } + + /** + * Get limit from group configuration + */ + private int getGroupLimit(Player player) { + if (!luckPermsAvailable || luckPerms == null) { + return -1; + } + + try { + // Use reflection to access LuckPerms API + Object userManager = luckPerms.getClass().getMethod("getUserManager").invoke(luckPerms); + Object user = userManager.getClass().getMethod("getUser", java.util.UUID.class).invoke(userManager, player.getUniqueId()); + + if (user == null) { + return -1; + } + + Map groupLimits = ConfigManager.getGroupLimits(); + + // Get player's primary group first + String primaryGroup = (String) user.getClass().getMethod("getPrimaryGroup").invoke(user); + if (groupLimits.containsKey(primaryGroup.toLowerCase())) { + return groupLimits.get(primaryGroup.toLowerCase()); + } + + // Check all groups the player is in (simplified - just check primary group for now) + } catch (Exception e) { + plugin.getLogger().warning("Error getting group limit for " + player.getName() + ": " + e.getMessage()); + } + + return -1; + } + + /** + * Check if LuckPerms is available + */ + public boolean isLuckPermsAvailable() { + return luckPermsAvailable; + } +} diff --git a/src/main/java/com/bhomes/plugin/managers/TeleportManager.java b/src/main/java/com/bhomes/plugin/managers/TeleportManager.java new file mode 100644 index 0000000..c823941 --- /dev/null +++ b/src/main/java/com/bhomes/plugin/managers/TeleportManager.java @@ -0,0 +1,169 @@ +package com.bhomes.plugin.managers; + +import com.bhomes.plugin.BHomesPlugin; +import com.bhomes.plugin.database.models.Home; +import com.bhomes.plugin.utils.ConfigManager; +import com.bhomes.plugin.utils.MessageUtil; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitTask; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class TeleportManager { + + private final BHomesPlugin plugin; + private final Map pendingTeleports = new HashMap<>(); + + public TeleportManager(BHomesPlugin plugin) { + this.plugin = plugin; + } + + /** + * Teleport a player to a home with delay + */ + public void teleportToHome(Player player, Home home) { + teleportToLocation(player, home.getLocation(), home.getName()); + } + + /** + * Teleport a player to a location with delay (used by TPA) + */ + public void teleportToLocation(Player player, Location target) { + teleportToLocation(player, target, null); + } + + private void teleportToLocation(Player player, Location target, String label) { + int delay = ConfigManager.getTeleportDelay(); + String displayName = (label != null) ? label : target.getWorld().getName() + " @ " + MessageUtil.formatLocation(target.getX(), target.getY(), target.getZ()); + + // Check if player can bypass delay + if (delay <= 0 || player.hasPermission(ConfigManager.getTeleportDelayBypassPermission())) { + // Instant teleport + executeTeleport(player, target, displayName); + return; + } + + // Cancel any existing teleport + cancelTeleport(player); + + // Store player's current location + Location startLocation = player.getLocation().clone(); + + // Send initial message + MessageUtil.sendMessage(player, "teleport-delay", + Map.of("delay", String.valueOf(delay), "home", displayName)); + + // Create countdown task + TeleportRequest request = new TeleportRequest(player, target, displayName, startLocation, delay); + pendingTeleports.put(player.getUniqueId(), request); + + // Start countdown using player's scheduler (Folia-compatible) + request.task = player.getScheduler().runAtFixedRate(plugin, scheduledTask -> { + // Check if player moved + if (ConfigManager.isCancelOnMove() && hasMoved(startLocation, player.getLocation())) { + cancelTeleport(player); + MessageUtil.sendMessage(player, "teleport-cancelled"); + return; + } + + request.remainingSeconds--; + + if (request.remainingSeconds <= 0) { + // Teleport! + executeTeleport(player, target, displayName); + pendingTeleports.remove(player.getUniqueId()); + scheduledTask.cancel(); + } else if (request.remainingSeconds <= 3) { + // Show countdown for last 3 seconds + MessageUtil.sendMessage(player, "teleport-countdown", + Map.of("delay", String.valueOf(request.remainingSeconds))); + } + }, null, 20L, 20L); // Run every second (20 ticks) + } + + /** + * Execute the actual teleport to a home + */ + private void executeTeleport(Player player, Home home) { + executeTeleport(player, home.getLocation(), home.getName()); + } + + /** + * Execute the actual teleport to a location + */ + private void executeTeleport(Player player, Location target, String label) { + player.teleportAsync(target).thenAccept(success -> { + if (success) { + MessageUtil.sendMessage(player, "teleport-success", Map.of("home", label)); + plugin.getCooldownManager().setCooldown(player); + } else { + MessageUtil.sendMessage(player, "teleport-failed", Map.of("home", label)); + } + }); + } + + /** + * Cancel a pending teleport + */ + public void cancelTeleport(Player player) { + TeleportRequest request = pendingTeleports.remove(player.getUniqueId()); + if (request != null && request.task != null) { + request.task.cancel(); + } + } + + /** + * Check if a player has moved + */ + private boolean hasMoved(Location from, Location to) { + if (!from.getWorld().equals(to.getWorld())) { + return true; + } + + // Allow small movements (like looking around) + double distance = from.distance(to); + return distance > 0.1; + } + + /** + * Check if a player has a pending teleport + */ + public boolean hasPendingTeleport(Player player) { + return pendingTeleports.containsKey(player.getUniqueId()); + } + + /** + * Clear all pending teleports + */ + public void clearAllTeleports() { + for (TeleportRequest request : pendingTeleports.values()) { + if (request.task != null) { + request.task.cancel(); + } + } + pendingTeleports.clear(); + } + + /** + * Internal class to track teleport requests + */ + private static class TeleportRequest { + Player player; + Location target; + String label; + Location startLocation; + int remainingSeconds; + io.papermc.paper.threadedregions.scheduler.ScheduledTask task; + + TeleportRequest(Player player, Location target, String label, Location startLocation, int delay) { + this.player = player; + this.target = target; + this.label = label; + this.startLocation = startLocation; + this.remainingSeconds = delay; + } + } +} diff --git a/src/main/java/com/bhomes/plugin/managers/TpaManager.java b/src/main/java/com/bhomes/plugin/managers/TpaManager.java new file mode 100644 index 0000000..d023bef --- /dev/null +++ b/src/main/java/com/bhomes/plugin/managers/TpaManager.java @@ -0,0 +1,256 @@ +package com.bhomes.plugin.managers; + +import com.bhomes.plugin.BHomesPlugin; +import com.bhomes.plugin.utils.ConfigManager; +import com.bhomes.plugin.utils.MessageUtil; +import org.bukkit.entity.Player; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class TpaManager { + + private final BHomesPlugin plugin; + private final Map pendingRequests = new ConcurrentHashMap<>(); + private final Map> blockedCache = new ConcurrentHashMap<>(); + + public enum RequestType { + TPA, // sender wants to go to target + TPA_HERE // sender wants target to come to them + } + + public TpaManager(BHomesPlugin plugin) { + this.plugin = plugin; + } + + /** + * Send a TPA request from sender to target + */ + public boolean sendRequest(Player sender, Player target, RequestType type) { + // Check if sender is blocked by target + if (isBlocked(target.getUniqueId(), sender.getUniqueId())) { + MessageUtil.sendMessage(sender, "tpa-blocked", Map.of("player", target.getName())); + return false; + } + + // Cancel any existing request from this sender to this target + UUID requestKey = getRequestKey(sender.getUniqueId(), target.getUniqueId()); + pendingRequests.remove(requestKey); + + // Create new request + TpaRequest request = new TpaRequest( + sender.getUniqueId(), + target.getUniqueId(), + sender.getName(), + target.getName(), + type, + System.currentTimeMillis() + (ConfigManager.getTpaTimeout() * 1000L) + ); + pendingRequests.put(requestKey, request); + + // Notify sender + if (type == RequestType.TPA) { + MessageUtil.sendMessage(sender, "tpa-request-sent", Map.of("player", target.getName())); + } else { + MessageUtil.sendMessage(sender, "tpahere-request-sent", Map.of("player", target.getName())); + } + + // Notify target + if (type == RequestType.TPA) { + MessageUtil.sendMessage(target, "tpa-request-received", Map.of("player", sender.getName())); + } else { + MessageUtil.sendMessage(target, "tpahere-request-received", Map.of("player", sender.getName())); + } + + return true; + } + + /** + * Accept a pending TPA request + */ + public boolean acceptRequest(Player target, Player sender) { + UUID requestKey = getRequestKey(sender.getUniqueId(), target.getUniqueId()); + TpaRequest request = pendingRequests.get(requestKey); + + if (request == null) { + MessageUtil.sendMessage(target, "tpa-no-request", Map.of("player", sender.getName())); + return false; + } + + // Check if expired + if (System.currentTimeMillis() > request.expiryTime) { + pendingRequests.remove(requestKey); + MessageUtil.sendMessage(target, "tpa-expired", Map.of("player", sender.getName())); + return false; + } + + pendingRequests.remove(requestKey); + + // Notify both players + MessageUtil.sendMessage(target, "tpa-accepted"); + MessageUtil.sendMessage(sender, "tpa-accepted-sender", Map.of("player", target.getName())); + + // Execute teleport + Player onlineSender = plugin.getServer().getPlayer(sender.getUniqueId()); + Player onlineTarget = plugin.getServer().getPlayer(target.getUniqueId()); + + if (onlineSender == null || onlineTarget == null) { + return false; + } + + if (request.type == RequestType.TPA) { + // Sender goes to target + plugin.getTeleportManager().teleportToLocation(onlineSender, onlineTarget.getLocation()); + } else { + // Target comes to sender + plugin.getTeleportManager().teleportToLocation(onlineTarget, onlineSender.getLocation()); + } + + return true; + } + + /** + * Deny a pending TPA request + */ + public boolean denyRequest(Player target, Player sender) { + UUID requestKey = getRequestKey(sender.getUniqueId(), target.getUniqueId()); + TpaRequest request = pendingRequests.remove(requestKey); + + if (request == null) { + MessageUtil.sendMessage(target, "tpa-no-request", Map.of("player", sender.getName())); + return false; + } + + MessageUtil.sendMessage(target, "tpa-denied"); + + Player onlineSender = plugin.getServer().getPlayer(sender.getUniqueId()); + if (onlineSender != null) { + MessageUtil.sendMessage(onlineSender, "tpa-denied-sender", Map.of("player", target.getName())); + } + + return true; + } + + /** + * Check if a player has a pending request from a sender + */ + public boolean hasPendingRequest(UUID senderUuid, UUID targetUuid) { + UUID requestKey = getRequestKey(senderUuid, targetUuid); + TpaRequest request = pendingRequests.get(requestKey); + if (request == null) return false; + if (System.currentTimeMillis() > request.expiryTime) { + pendingRequests.remove(requestKey); + return false; + } + return true; + } + + /** + * Get all pending requests for a player (as target) + */ + public List getPendingRequestsFor(Player player) { + List requests = new ArrayList<>(); + long now = System.currentTimeMillis(); + pendingRequests.values().removeIf(r -> now > r.expiryTime); + for (TpaRequest request : pendingRequests.values()) { + if (request.targetUuid.equals(player.getUniqueId())) { + requests.add(request); + } + } + return requests; + } + + // ========== Blocked player management ========== + + /** + * Check if a player has blocked another + */ + public boolean isBlocked(UUID playerUuid, UUID blockedUuid) { + Set blocked = blockedCache.get(playerUuid); + if (blocked != null) { + return blocked.contains(blockedUuid); + } + // Load from database + Set fromDb = plugin.getDatabaseManager().getBlockedPlayers(playerUuid); + blockedCache.put(playerUuid, fromDb); + return fromDb.contains(blockedUuid); + } + + /** + * Block a player + */ + public void blockPlayer(Player player, Player target) { + plugin.getDatabaseManager().blockPlayer(player.getUniqueId(), target.getUniqueId()); + blockedCache.computeIfAbsent(player.getUniqueId(), k -> ConcurrentHashMap.newKeySet()).add(target.getUniqueId()); + MessageUtil.sendMessage(player, "tpa-blocked-add", Map.of("player", target.getName())); + } + + /** + * Unblock a player + */ + public void unblockPlayer(Player player, Player target) { + plugin.getDatabaseManager().unblockPlayer(player.getUniqueId(), target.getUniqueId()); + Set blocked = blockedCache.get(player.getUniqueId()); + if (blocked != null) { + blocked.remove(target.getUniqueId()); + } + MessageUtil.sendMessage(player, "tpa-blocked-remove", Map.of("player", target.getName())); + } + + /** + * Toggle block status for a player + */ + public void toggleBlockPlayer(Player player, Player target) { + if (isBlocked(player.getUniqueId(), target.getUniqueId())) { + unblockPlayer(player, target); + } else { + blockPlayer(player, target); + } + } + + /** + * Get list of blocked player UUIDs + */ + public Set getBlockedPlayers(UUID playerUuid) { + Set blocked = blockedCache.get(playerUuid); + if (blocked != null) return blocked; + Set fromDb = plugin.getDatabaseManager().getBlockedPlayers(playerUuid); + blockedCache.put(playerUuid, fromDb); + return fromDb; + } + + /** + * Clear expired requests (run periodically) + */ + public void cleanExpiredRequests() { + long now = System.currentTimeMillis(); + pendingRequests.values().removeIf(r -> now > r.expiryTime); + } + + private UUID getRequestKey(UUID senderUuid, UUID targetUuid) { + // Use concatenation to create a unique key + String key = senderUuid.toString() + ":" + targetUuid.toString(); + return UUID.nameUUIDFromBytes(key.getBytes()); + } + + /** + * Internal class representing a TPA request + */ + public static class TpaRequest { + public final UUID senderUuid; + public final UUID targetUuid; + public final String senderName; + public final String targetName; + public final RequestType type; + public final long expiryTime; + + TpaRequest(UUID senderUuid, UUID targetUuid, String senderName, String targetName, RequestType type, long expiryTime) { + this.senderUuid = senderUuid; + this.targetUuid = targetUuid; + this.senderName = senderName; + this.targetName = targetName; + this.type = type; + this.expiryTime = expiryTime; + } + } +} diff --git a/src/main/java/com/bhomes/plugin/utils/ConfigManager.java b/src/main/java/com/bhomes/plugin/utils/ConfigManager.java new file mode 100644 index 0000000..b539474 --- /dev/null +++ b/src/main/java/com/bhomes/plugin/utils/ConfigManager.java @@ -0,0 +1,82 @@ +package com.bhomes.plugin.utils; + +import com.bhomes.plugin.BHomesPlugin; +import org.bukkit.configuration.file.FileConfiguration; + +import java.util.HashMap; +import java.util.Map; + +public class ConfigManager { + + private static BHomesPlugin plugin; + private static FileConfiguration config; + + public static void init(BHomesPlugin pluginInstance) { + plugin = pluginInstance; + config = plugin.getConfig(); + } + + // Database settings + public static String getDatabaseType() { + return config.getString("database.type", "sqlite"); + } + + public static String getDatabaseFile() { + return config.getString("database.file", "homes.db"); + } + + // Limit settings + public static int getDefaultLimit() { + return config.getInt("limits.default", 3); + } + + public static Map getGroupLimits() { + Map limits = new HashMap<>(); + if (config.contains("limits.groups")) { + for (String group : config.getConfigurationSection("limits.groups").getKeys(false)) { + limits.put(group.toLowerCase(), config.getInt("limits.groups." + group)); + } + } + return limits; + } + + // Cooldown settings + public static boolean isCooldownEnabled() { + return config.getBoolean("cooldown.enabled", true); + } + + public static int getCooldownDuration() { + return config.getInt("cooldown.duration", 60); + } + + public static String getCooldownBypassPermission() { + return config.getString("cooldown.bypass-permission", "bhomes.bypass.cooldown"); + } + + // Teleport delay settings + public static int getTeleportDelay() { + return config.getInt("teleport.delay", 5); + } + + public static boolean isCancelOnMove() { + return config.getBoolean("teleport.cancel-on-move", true); + } + + public static String getTeleportDelayBypassPermission() { + return config.getString("teleport.bypass-permission", "bhomes.bypass.delay"); + } + + // TPA settings + public static int getTpaTimeout() { + return config.getInt("tpa.timeout", 60); + } + + // Message settings + public static String getMessage(String key) { + return config.getString("messages." + key, "Message not found: " + key); + } + + public static String getPrefix() { + return config.getString("messages.prefix", "[BHomes] "); + } +} diff --git a/src/main/java/com/bhomes/plugin/utils/MessageUtil.java b/src/main/java/com/bhomes/plugin/utils/MessageUtil.java new file mode 100644 index 0000000..2b9fc36 --- /dev/null +++ b/src/main/java/com/bhomes/plugin/utils/MessageUtil.java @@ -0,0 +1,96 @@ +package com.bhomes.plugin.utils; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.command.CommandSender; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class MessageUtil { + + private static final MiniMessage miniMessage = MiniMessage.miniMessage(); + + /** + * Send a message to a command sender with placeholder replacement + */ + public static void sendMessage(CommandSender sender, String messageKey, Map placeholders) { + String message = ConfigManager.getMessage(messageKey); + message = replacePlaceholders(message, placeholders); + Component component = miniMessage.deserialize(message); + sender.sendMessage(component); + } + + /** + * Send a message to a command sender without placeholders + */ + public static void sendMessage(CommandSender sender, String messageKey) { + sendMessage(sender, messageKey, Map.of()); + } + + /** + * Parse a string with MiniMessage and placeholders + */ + public static Component parseMessage(String message, Map placeholders) { + message = replacePlaceholders(message, placeholders); + return miniMessage.deserialize(message); + } + + /** + * Parse a string with MiniMessage without placeholders + */ + public static Component parseMessage(String message) { + return miniMessage.deserialize(message); + } + + /** + * Parse a list of strings with MiniMessage and placeholders + */ + public static List parseMessages(List messages, Map placeholders) { + List components = new ArrayList<>(); + for (String message : messages) { + components.add(parseMessage(message, placeholders)); + } + return components; + } + + /** + * Replace placeholders in a string + */ + private static String replacePlaceholders(String message, Map placeholders) { + // Replace prefix first + message = message.replace("{prefix}", ConfigManager.getPrefix()); + + // Replace custom placeholders + for (Map.Entry entry : placeholders.entrySet()) { + message = message.replace("{" + entry.getKey() + "}", entry.getValue()); + } + + return message; + } + + /** + * Format a location string + */ + public static String formatLocation(double x, double y, double z) { + return String.format("%.1f, %.1f, %.1f", x, y, z); + } + + /** + * Format a time duration in seconds + */ + public static String formatDuration(long seconds) { + if (seconds < 60) { + return seconds + "s"; + } else if (seconds < 3600) { + long minutes = seconds / 60; + long secs = seconds % 60; + return minutes + "m " + secs + "s"; + } else { + long hours = seconds / 3600; + long minutes = (seconds % 3600) / 60; + return hours + "h " + minutes + "m"; + } + } +} diff --git a/src/main/java/com/bhomes/plugin/utils/TeleportUtil.java b/src/main/java/com/bhomes/plugin/utils/TeleportUtil.java new file mode 100644 index 0000000..63fade3 --- /dev/null +++ b/src/main/java/com/bhomes/plugin/utils/TeleportUtil.java @@ -0,0 +1,56 @@ +package com.bhomes.plugin.utils; + +import com.bhomes.plugin.database.models.Home; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class TeleportUtil { + + /** + * Safely teleport a player to a home + */ + public static boolean teleportToHome(Player player, Home home) { + try { + Location location = home.getLocation(); + if (location == null || location.getWorld() == null) { + MessageUtil.sendMessage(player, "teleport-failed", + Map.of("home", home.getName())); + return false; + } + + // Teleport the player + player.teleportAsync(location).thenAccept(success -> { + if (success) { + MessageUtil.sendMessage(player, "teleport-success", + Map.of("home", home.getName())); + } else { + MessageUtil.sendMessage(player, "teleport-failed", + Map.of("home", home.getName())); + } + }); + + return true; + } catch (Exception e) { + MessageUtil.sendMessage(player, "teleport-failed", + Map.of("home", home.getName())); + return false; + } + } + + /** + * Check if a location is safe for teleportation + */ + public static boolean isSafeLocation(Location location) { + if (location == null || location.getWorld() == null) { + return false; + } + + // Check if the block at the location and above it are passable + Location feet = location.clone(); + Location head = location.clone().add(0, 1, 0); + + return feet.getBlock().isPassable() && head.getBlock().isPassable(); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..904c78d --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,85 @@ +# BHomes Configuration + +database: + type: sqlite + file: homes.db + +limits: + # Default home limit for all players + default: 3 + # Group-based limits (requires LuckPerms) + groups: + vip: 4 + vip+: 5 + +cooldown: + enabled: true + duration: 10 # seconds + bypass-permission: bhomes.bypass.cooldown + +teleport: + delay: 5 # seconds - delay before teleporting (0 to disable) + cancel-on-move: true # cancel teleport if player moves + bypass-permission: bhomes.bypass.delay + +tpa: + # Timeout in seconds before a TPA request expires + timeout: 60 + +messages: + prefix: "[BHomes] " + + # Home management messages + home-set: "{prefix}Home {home} set!" + home-deleted: "{prefix}Home {home} deleted!" + home-not-found: "{prefix}Home {home} not found!" + home-already-exists: "{prefix}You already have a home named {home}!" + home-list: "{prefix}Your homes: {homes}" + home-list-empty: "{prefix}You don't have any homes set yet. Use /sethome to create one." + + # Teleport messages + teleporting: "{prefix}Teleporting to {home}..." + teleport-success: "{prefix}Teleported to {home}!" + teleport-failed: "{prefix}Failed to teleport to {home}!" + + # Cooldown messages + cooldown: "{prefix}Wait {cooldown}s before teleporting again." + + # Teleport delay messages + teleport-delay: "{prefix}Teleporting in {delay} seconds... Don't move!" + teleport-cancelled: "{prefix}Teleport cancelled! You moved." + teleport-countdown: "{prefix}{delay}..." + + # Limit messages + limit-reached: "{prefix}You've reached your home limit ({limit})!" + + # TPA messages + tpa-request-sent: "{prefix}TPA request sent to {player}!" + tpahere-request-sent: "{prefix}TPA here request sent to {player}!" + tpa-request-received: "{prefix}{player} wants to teleport to you! [ACCEPT] [DENY]" + tpahere-request-received: "{prefix}{player} wants you to teleport to them! [ACCEPT] [DENY]" + tpa-accepted: "{prefix}Request accepted!" + tpa-accepted-sender: "{prefix}{player} accepted your teleport request!" + tpa-denied: "{prefix}Request denied!" + tpa-denied-sender: "{prefix}{player} denied your teleport request!" + tpa-no-request: "{prefix}No pending request from {player}!" + tpa-no-requests: "{prefix}You have no pending teleport requests!" + tpa-expired: "{prefix}The teleport request from {player} has expired!" + tpa-self: "{prefix}You can't send a teleport request to yourself!" + tpa-blocked: "{prefix}{player} has blocked teleport requests from you!" + tpa-blocked-add: "{prefix}Blocked teleport requests from {player}!" + tpa-blocked-remove: "{prefix}Unblocked teleport requests from {player}!" + + # Error messages + invalid-name: "{prefix}Invalid home name! Use only letters, numbers, and underscores (max 16 characters)." + player-not-found: "{prefix}Player {player} not found!" + no-permission: "{prefix}You don't have permission to do that!" + must-be-player: "{prefix}This command can only be used by players!" + + # Admin messages + admin-deleted: "{prefix}Deleted {player}'s home {home}!" + admin-teleported: "{prefix}Teleported to {player}'s home {home}!" + admin-list: "{prefix}{player}'s homes: {homes}" + admin-reload: "{prefix}Configuration reloaded!" + admin-export: "{prefix}Exported {count} homes to {file}!" + admin-import: "{prefix}Imported {count} homes from {file}!" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..a7ee8c9 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,59 @@ +name: BHomes +version: 1.0.0 +main: com.bhomes.plugin.BHomesPlugin +description: A simple home management plugin with database persistence and LuckPerms integration. +author: SiDa +website: https://github.com/bhomes/plugin +api-version: '1.21' +softdepend: [LuckPerms] +folia-supported: true + +permissions: + bhomes.sethome: + description: Set homes at your current location + default: true + bhomes.home: + description: Teleport to your homes + default: true + bhomes.delhome: + description: Delete your homes + default: true + bhomes.bypass.cooldown: + description: Bypass teleport cooldown + default: op + bhomes.bypass.delay: + description: Bypass teleport delay + default: op + bhomes.admin.*: + description: All admin commands + default: op + children: + bhomes.admin.list: true + bhomes.admin.delete: true + bhomes.admin.teleport: true + bhomes.admin.reload: true + bhomes.admin.import: true + bhomes.admin.export: true + bhomes.admin.list: + description: View all player homes + default: op + bhomes.admin.delete: + description: Delete any player's home + default: op + bhomes.admin.teleport: + description: Teleport to any player's home + default: op + bhomes.admin.reload: + description: Reload plugin configuration + default: op + bhomes.admin.import: + description: Import homes data + default: op + bhomes.admin.export: + description: Export homes data + default: op + bhomes.tpa: + description: Use teleport request commands + default: true + +# No need to add "commands", as it would conflict with the new PaperMC command registration system.