From 32f1eb25286625ae05771abee3063cd40664901b Mon Sep 17 00:00:00 2001 From: Shun Ueda Date: Sun, 12 Oct 2025 15:27:41 -0400 Subject: [PATCH] impl --- build.gradle.kts | 1 + plugins/TokenManager/LICENSE | 674 ++++++++++++++++++ plugins/TokenManager/README.md | 73 ++ plugins/TokenManager/build.gradle.kts | 52 ++ plugins/TokenManager/gradle.properties | 1 + plugins/TokenManager/settings.gradle.kts | 1 + .../me/realized/tokenmanager/Permissions.java | 23 + .../tokenmanager/TokenManagerPlugin.java | 399 +++++++++++ .../tokenmanager/api/TokenManager.java | 133 ++++ .../tokenmanager/api/event/TMEvent.java | 35 + .../api/event/TMSellAllEvent.java | 22 + .../api/event/TMSellHandEvent.java | 30 + .../api/event/TMShopPurchaseEvent.java | 38 + .../api/event/TMTokenSendEvent.java | 29 + .../tokenmanager/command/BaseCommand.java | 74 ++ .../command/commands/TMCommand.java | 32 + .../command/commands/TokenCommand.java | 51 ++ .../commands/subcommands/BalanceCommand.java | 62 ++ .../commands/subcommands/GiveAllCommand.java | 49 ++ .../commands/subcommands/OfflineCommand.java | 178 +++++ .../commands/subcommands/OpenCommand.java | 55 ++ .../commands/subcommands/ReloadCommand.java | 75 ++ .../commands/subcommands/SellCommand.java | 115 +++ .../commands/subcommands/SendCommand.java | 109 +++ .../commands/subcommands/ShopCommand.java | 74 ++ .../commands/subcommands/ShopsCommand.java | 27 + .../commands/subcommands/TopCommand.java | 50 ++ .../commands/subcommands/TransferCommand.java | 19 + .../commands/subcommands/VersionCommand.java | 23 + .../commands/subcommands/WorthCommand.java | 94 +++ .../realized/tokenmanager/config/Config.java | 114 +++ .../me/realized/tokenmanager/config/Lang.java | 200 ++++++ .../tokenmanager/config/WorthConfig.java | 105 +++ .../tokenmanager/data/DataManager.java | 216 ++++++ .../data/database/AbstractDatabase.java | 61 ++ .../tokenmanager/data/database/Database.java | 89 +++ .../data/database/FileDatabase.java | 279 ++++++++ .../data/database/MySQLDatabase.java | 539 ++++++++++++++ .../tokenmanager/event/TokenReceiveEvent.java | 61 ++ .../tokenmanager/hook/HookManager.java | 22 + .../hook/hooks/PlaceholderHook.java | 52 ++ .../tokenmanager/hook/hooks/VaultHook.java | 272 +++++++ .../me/realized/tokenmanager/shop/Shop.java | 83 +++ .../tokenmanager/shop/ShopConfig.java | 202 ++++++ .../tokenmanager/shop/ShopManager.java | 137 ++++ .../me/realized/tokenmanager/shop/Slot.java | 182 +++++ .../tokenmanager/shop/gui/BaseGui.java | 48 ++ .../shop/gui/guis/ConfirmGui.java | 72 ++ .../tokenmanager/shop/gui/guis/ShopGui.java | 106 +++ .../realized/tokenmanager/util/EnumUtil.java | 22 + .../realized/tokenmanager/util/Loadable.java | 8 + .../me/realized/tokenmanager/util/Log.java | 37 + .../tokenmanager/util/NumberUtil.java | 90 +++ .../tokenmanager/util/Placeholders.java | 61 ++ .../tokenmanager/util/Reloadable.java | 3 + .../tokenmanager/util/StringUtil.java | 86 +++ .../tokenmanager/util/UpdateChecker.java | 46 ++ .../util/command/AbstractCommand.java | 180 +++++ .../tokenmanager/util/compat/CompatBase.java | 50 ++ .../tokenmanager/util/compat/CompatUtil.java | 23 + .../tokenmanager/util/compat/Items.java | 48 ++ .../util/compat/ReflectionUtil.java | 152 ++++ .../tokenmanager/util/compat/Skulls.java | 26 + .../tokenmanager/util/compat/SpawnEggs.java | 38 + .../tokenmanager/util/compat/Terracottas.java | 35 + .../util/config/AbstractConfiguration.java | 209 ++++++ .../util/config/convert/Converter.java | 8 + .../util/hook/AbstractHookManager.java | 53 ++ .../tokenmanager/util/hook/PluginHook.java | 19 + .../util/inventory/GUIBuilder.java | 98 +++ .../util/inventory/InventoryUtil.java | 50 ++ .../util/inventory/ItemBuilder.java | 55 ++ .../tokenmanager/util/inventory/ItemUtil.java | 335 +++++++++ .../util/profile/NameFetcher.java | 129 ++++ .../util/profile/ProfileUtil.java | 55 ++ .../util/profile/UUIDFetcher.java | 84 +++ .../src/main/resources/config.yml | 81 +++ .../TokenManager/src/main/resources/lang.yml | 89 +++ .../src/main/resources/plugin.yml | 36 + .../TokenManager/src/main/resources/shops.yml | 120 ++++ .../TokenManager/src/main/resources/worth.yml | 3 + 81 files changed, 7767 insertions(+) create mode 100644 plugins/TokenManager/LICENSE create mode 100644 plugins/TokenManager/README.md create mode 100644 plugins/TokenManager/build.gradle.kts create mode 100644 plugins/TokenManager/gradle.properties create mode 100644 plugins/TokenManager/settings.gradle.kts create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/Permissions.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/TokenManagerPlugin.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/TokenManager.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMEvent.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMSellAllEvent.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMSellHandEvent.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMShopPurchaseEvent.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMTokenSendEvent.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/BaseCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/TMCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/TokenCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/BalanceCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/GiveAllCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/OfflineCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/OpenCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/ReloadCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/SellCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/SendCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/ShopCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/ShopsCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/TopCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/TransferCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/VersionCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/WorthCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/config/Config.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/config/Lang.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/config/WorthConfig.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/DataManager.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/AbstractDatabase.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/Database.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/FileDatabase.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/MySQLDatabase.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/event/TokenReceiveEvent.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/hook/HookManager.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/hook/hooks/PlaceholderHook.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/hook/hooks/VaultHook.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/Shop.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/ShopConfig.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/ShopManager.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/Slot.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/gui/BaseGui.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/gui/guis/ConfirmGui.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/gui/guis/ShopGui.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/EnumUtil.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Loadable.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Log.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/NumberUtil.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Placeholders.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Reloadable.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/StringUtil.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/UpdateChecker.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/command/AbstractCommand.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/CompatBase.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/CompatUtil.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Items.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/ReflectionUtil.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Skulls.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/SpawnEggs.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Terracottas.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/config/AbstractConfiguration.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/config/convert/Converter.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/hook/AbstractHookManager.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/hook/PluginHook.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/GUIBuilder.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/InventoryUtil.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/ItemBuilder.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/ItemUtil.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/profile/NameFetcher.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/profile/ProfileUtil.java create mode 100644 plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/profile/UUIDFetcher.java create mode 100644 plugins/TokenManager/src/main/resources/config.yml create mode 100644 plugins/TokenManager/src/main/resources/lang.yml create mode 100644 plugins/TokenManager/src/main/resources/plugin.yml create mode 100644 plugins/TokenManager/src/main/resources/shops.yml create mode 100644 plugins/TokenManager/src/main/resources/worth.yml diff --git a/build.gradle.kts b/build.gradle.kts index 9bf70f0a..0f43e94a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ import nl.littlerobots.vcu.plugin.resolver.VersionSelectors plugins { + java alias(libs.plugins.spotless) alias(libs.plugins.version.catalog.update) } diff --git a/plugins/TokenManager/LICENSE b/plugins/TokenManager/LICENSE new file mode 100644 index 00000000..20d40b6b --- /dev/null +++ b/plugins/TokenManager/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/plugins/TokenManager/README.md b/plugins/TokenManager/README.md new file mode 100644 index 00000000..ee289366 --- /dev/null +++ b/plugins/TokenManager/README.md @@ -0,0 +1,73 @@ +

TokenManager

+ +[![](https://jitpack.io/v/Realizedd/TokenManager.svg)](https://jitpack.io/#Realizedd/TokenManager) + +A simple economy plugin for spigot. Spigot Project Page + +--- + +* **[Wiki](https://github.com/Realizedd/TokenManager/wiki)** +* **[Commands](https://github.com/Realizedd/TokenManager/wiki/commands)** +* **[Permissions](https://github.com/Realizedd/TokenManager/wiki/permissions)** +* **[config.yml](https://github.com/Realizedd/TokenManager/blob/master/src/main/resources/config.yml)** +* **[lang.yml](https://github.com/Realizedd/TokenManager/blob/master/src/main/resources/lang.yml)** +* **[shops.yml](https://github.com/Realizedd/TokenManager/blob/master/src/main/resources/shops.yml)** +* **[Support Discord](https://discord.gg/RNy45sg)** + + +### Getting the dependency + +#### Repository +Gradle: +```groovy +maven { + name 'jitpack-repo' + url 'https://jitpack.io' +} +``` + +Maven: +```xml + + jitpack-repo + https://jitpack.io + +``` + +#### Dependency +Gradle: +```groovy +compile (group: 'com.github.Realizedd', name: 'TokenManager', version: '3.2.4') { + transitive = false +} +``` + +Maven: +```xml + + com.github.Realizedd + TokenManager + 3.2.4 + + + * + * + + + +``` + +### plugin.yml +Add TokenManager as a soft-depend to ensure TokenManager is fully loaded before your plugin. +```yaml +soft-depend: [TokenManager] +``` + +### Getting the API instance + +```java +@Override +public void onEnable() { + TokenManager api = (TokenManager) Bukkit.getServer().getPluginManager().getPlugin("TokenManager"); +} +``` diff --git a/plugins/TokenManager/build.gradle.kts b/plugins/TokenManager/build.gradle.kts new file mode 100644 index 00000000..1836401c --- /dev/null +++ b/plugins/TokenManager/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + java + id("com.gradleup.shadow") +} + +repositories { + mavenCentral() + maven("https://hub.spigotmc.org/nexus/content/repositories/public") + maven("https://oss.sonatype.org/content/repositories/snapshots") + maven("https://repo.purpurmc.org/snapshots") + maven("https://nexus.frengor.com/repository/public/") + maven("https://nexus.scarsz.me/content/groups/public/") + maven("https://repo.codemc.io/repository/maven-public/") + maven("https://repo.inventivetalent.org/content/groups/public/") + maven { url = uri("https://repo.extendedclip.com/content/repositories/placeholderapi/") } +} + +dependencies { + compileOnly("org.projectlombok:lombok:1.18.42") + annotationProcessor("org.projectlombok:lombok:1.18.42") + implementation(libs.purpur.api) + implementation("me.clip:placeholderapi:2.10.9") + implementation(libs.vault.api) + implementation("org.apache.commons:commons-lang3:3.19.0") + + // shaded dependencies + implementation("redis.clients:jedis:3.5.1") + implementation("org.apache.commons:commons-pool2:2.4.2") + implementation("com.zaxxer:HikariCP:4.0.2") + implementation("org.slf4j:slf4j-nop:1.7.32") +} + +tasks.apply { + jar { enabled = false } + + shadowJar { + archiveBaseName.set(project.name) + archiveClassifier.set("") + archiveVersion.set("") + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + } + + build { dependsOn(shadowJar) } + + processResources { + val version: String by project + val properties = mapOf("version" to version) + inputs.properties(properties) + filteringCharset = Charsets.UTF_8.name() + filesMatching("plugin.yml") { expand(properties) } + } +} diff --git a/plugins/TokenManager/gradle.properties b/plugins/TokenManager/gradle.properties new file mode 100644 index 00000000..40cc1bb7 --- /dev/null +++ b/plugins/TokenManager/gradle.properties @@ -0,0 +1 @@ +version=1.0-SNAPSHOT diff --git a/plugins/TokenManager/settings.gradle.kts b/plugins/TokenManager/settings.gradle.kts new file mode 100644 index 00000000..f9798d97 --- /dev/null +++ b/plugins/TokenManager/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "TokenManager" diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/Permissions.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/Permissions.java new file mode 100644 index 00000000..c0129fc3 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/Permissions.java @@ -0,0 +1,23 @@ +package me.realized.tokenmanager; + +public final class Permissions { + + private static final String PREFIX = "tokenmanager."; + + public static final String CMD_TOKEN = PREFIX + "use"; + public static final String CMD_ADMIN = PREFIX + "admin"; + public static final String CMD_BALANCE_OTHERS = PREFIX + "use.balance.others"; + public static final String CMD_SELL = PREFIX + "use.sell"; + public static final String CMD_SELL_ALL = PREFIX + "use.sell.all"; + public static final String CMD_SEND = PREFIX + "use.send"; + public static final String CMD_SHOP = PREFIX + "use.shop"; + public static final String CMD_TOP = PREFIX + "use.top"; + public static final String CMD_WORTH = PREFIX + "use.worth"; + public static final String CMD_WORTH_ALL = PREFIX + "use.worth.all"; + + public static final String SHOP = PREFIX + "use.shop."; + public static final String SHOP_SLOT_OLD = PREFIX + "use."; + public static final String SHOP_SLOT_CANCEL = PREFIX + "cancel."; + + private Permissions() {} +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/TokenManagerPlugin.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/TokenManagerPlugin.java new file mode 100644 index 00000000..cb42f9d4 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/TokenManagerPlugin.java @@ -0,0 +1,399 @@ +package me.realized.tokenmanager; + +import com.google.common.collect.Lists; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.stream.Collectors; +import lombok.Getter; +import me.realized.tokenmanager.api.TokenManager; +import me.realized.tokenmanager.command.commands.TMCommand; +import me.realized.tokenmanager.command.commands.TokenCommand; +import me.realized.tokenmanager.command.commands.subcommands.OfflineCommand.ModifyType; +import me.realized.tokenmanager.config.Config; +import me.realized.tokenmanager.config.Lang; +import me.realized.tokenmanager.config.WorthConfig; +import me.realized.tokenmanager.data.DataManager; +import me.realized.tokenmanager.data.database.Database.TopElement; +import me.realized.tokenmanager.hook.HookManager; +import me.realized.tokenmanager.shop.Shop; +import me.realized.tokenmanager.shop.ShopConfig; +import me.realized.tokenmanager.shop.ShopManager; +import me.realized.tokenmanager.util.Loadable; +import me.realized.tokenmanager.util.Log; +import me.realized.tokenmanager.util.NumberUtil; +import me.realized.tokenmanager.util.Reloadable; +import me.realized.tokenmanager.util.StringUtil; +import me.realized.tokenmanager.util.UpdateChecker; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.java.JavaPlugin; + +public class TokenManagerPlugin extends JavaPlugin implements TokenManager, Listener { + + private static final int RESOURCE_ID = 8610; + private static final String ADMIN_UPDATE_MESSAGE = + "&9[TM] &bTokenManager &fv%s &7is now available for download! Download at: &c%s"; + private static final String RESOURCE_URL = + "https://www.spigotmc.org/resources/tokenmanager.8610/"; + + @Getter private static TokenManagerPlugin instance; + + private final List loadables = new ArrayList<>(); + private int lastLoad; + + @Getter private Config configuration; + @Getter private Lang lang; + @Getter private DataManager dataManager; + @Getter private ShopConfig shopConfig; + @Getter private ShopManager shopManager; + @Getter private WorthConfig worthConfig; + + private volatile boolean updateAvailable; + private volatile String newVersion; + + @Override + public void onEnable() { + instance = this; + Log.setSource(this); + loadables.add(configuration = new Config(this)); + loadables.add(lang = new Lang(this)); + loadables.add(dataManager = new DataManager(this)); + loadables.add(shopConfig = new ShopConfig(this)); + loadables.add(shopManager = new ShopManager(this)); + loadables.add(worthConfig = new WorthConfig(this)); + loadables.add(new HookManager(this)); + + if (!load()) { + getPluginLoader().disablePlugin(this); + return; + } + + new TMCommand(this).register(); + new TokenCommand(this).register(); + + if (!configuration.isCheckForUpdates()) { + return; + } + + final UpdateChecker updateChecker = new UpdateChecker(this, RESOURCE_ID); + updateChecker.check( + (hasUpdate, newVersion) -> { + if (hasUpdate) { + TokenManagerPlugin.this.updateAvailable = true; + TokenManagerPlugin.this.newVersion = newVersion; + Log.info("==============================================="); + Log.info("An update for " + getName() + " is available!"); + Log.info("Download " + getName() + " v" + newVersion + " here:"); + Log.info(getDescription().getWebsite()); + Log.info("==============================================="); + } else { + Log.info("No updates were available. You are on the latest version!"); + } + }); + getServer().getPluginManager().registerEvents(this, this); + } + + @Override + public void onDisable() { + instance = null; + unload(); + Log.setSource(null); + } + + /** + * @return true if load was successful, otherwise false + */ + private boolean load() { + for (final Loadable loadable : loadables) { + try { + loadable.handleLoad(); + lastLoad = loadables.indexOf(loadable); + Log.info("Loaded " + loadable.getClass().getSimpleName() + "."); + } catch (Exception ex) { + Log.error( + "There was an error while loading " + + loadable.getClass().getSimpleName() + + "! If you believe this is an issue from the plugin, please contact the developer."); + Log.error("Cause of error: " + ex.getMessage()); + ex.printStackTrace(); + return false; + } + } + + return true; + } + + /** + * @return true if unload was successful, otherwise false + */ + private boolean unload() { + for (final Loadable loadable : Lists.reverse(loadables)) { + try { + if (loadables.indexOf(loadable) > lastLoad) { + continue; + } + + loadable.handleUnload(); + Log.info("Unloaded " + loadable.getClass().getSimpleName() + "."); + } catch (Exception ex) { + Log.error( + "There was an error while unloading " + + loadable.getClass().getSimpleName() + + "! If you believe this is an issue from the plugin, please contact the developer."); + Log.error("Cause of error: " + ex.getMessage()); + ex.printStackTrace(); + return false; + } + } + + return true; + } + + public void doSync(final Runnable runnable) { + getServer().getScheduler().runTask(this, runnable); + } + + public int doSyncRepeat(final Runnable runnable, final long delay, final long period) { + return getServer().getScheduler().runTaskTimer(this, runnable, delay, period).getTaskId(); + } + + public void doAsync(final Runnable runnable) { + getServer().getScheduler().runTaskAsynchronously(this, runnable); + } + + public void doAsyncLater(final Runnable runnable, final long delay) { + getServer().getScheduler().runTaskLaterAsynchronously(this, runnable, delay); + } + + @Override + public Optional getShop(final String name) { + return shopConfig.getShop(name); + } + + @Override + public Optional getShop(final Inventory inventory) { + return shopManager.find(inventory); + } + + @Override + public OptionalLong getWorth(final Material material) { + return worthConfig.getWorth(material); + } + + @Override + public OptionalLong getWorth(final ItemStack item) { + return worthConfig.getWorth(item); + } + + @Override + public OptionalLong getTokens(final Player player) { + return dataManager.get(player); + } + + @Override + public void setTokens(final Player player, final long amount) { + dataManager.set(player, amount); + } + + @Override + public boolean addTokens(final Player player, final long amount) { + final OptionalLong balance = getTokens(player); + + if (!balance.isPresent()) { + return false; + } + + setTokens(player, balance.getAsLong() + amount); + return true; + } + + @Override + public boolean removeTokens(final Player player, final long amount) { + final OptionalLong balance = getTokens(player); + + if (!balance.isPresent()) { + return false; + } + + setTokens(player, balance.getAsLong() - amount); + return true; + } + + @Override + public void setTokens(final String key, final long amount) { + dataManager.set(key, ModifyType.SET, amount, amount, true, null, Log::error); + } + + @Override + public void addTokens(final String key, final long amount, final boolean silent) { + dataManager.get( + key, + balance -> { + if (!balance.isPresent()) { + return; + } + + final ModifyType type = ModifyType.ADD; + dataManager.set( + key, type, amount, type.apply(balance.getAsLong(), amount), silent, null, Log::error); + }, + Log::error); + } + + @Override + public void addTokens(final String key, final long amount) { + addTokens(key, amount, false); + } + + @Override + public void removeTokens(final String key, final long amount, final boolean silent) { + dataManager.get( + key, + balance -> { + if (!balance.isPresent()) { + return; + } + + final ModifyType type = ModifyType.REMOVE; + dataManager.set( + key, type, amount, type.apply(balance.getAsLong(), amount), silent, null, Log::error); + }, + Log::error); + } + + @Override + public void removeTokens(final String key, final long amount) { + removeTokens(key, amount, false); + } + + @Override + public boolean reload() { + if (!(unload() && load())) { + getPluginLoader().disablePlugin(this); + return false; + } + + return true; + } + + public boolean reload(final Loadable loadable) { + final String name = loadable.getClass().getSimpleName(); + boolean unloaded = false; + try { + loadable.handleUnload(); + unloaded = true; + Log.info("Unloaded " + name + "."); + loadable.handleLoad(); + Log.info("Loaded " + name + "."); + return true; + } catch (Exception ex) { + Log.error( + "There was an error while " + + (unloaded ? "loading " : "unloading ") + + name + + "! If you believe this is an issue from the plugin, please contact the developer."); + Log.error("Cause of error: " + ex.getMessage()); + ex.printStackTrace(); + return false; + } + } + + public Optional find(final String name) { + return loadables.stream() + .filter(loadable -> loadable.getClass().getSimpleName().equalsIgnoreCase(name)) + .findFirst(); + } + + public List getReloadables() { + return loadables.stream() + .filter(loadable -> loadable instanceof Reloadable) + .map(loadable -> loadable.getClass().getSimpleName()) + .collect(Collectors.toList()); + } + + public String handlePlaceholderRequest(final Player player, final String identifier) { + if (player == null) { + return "Player is required"; + } + + final long balance = dataManager.get(player).orElse(0); + + switch (identifier) { + case "tokens": + case "tokens_raw": + return String.valueOf(balance); + case "tokens_commas": + return NumberUtil.withCommas(balance); + case "tokens_formatted": + return NumberUtil.withSuffix(balance); + } + + if (identifier.equals("rank")) { + final List top = dataManager.getTopCache(); + + if (top == null) { + return lang.getMessage("PLACEHOLDER.rank.loading"); + } + + if (top.isEmpty()) { + return lang.getMessage("PLACEHOLDER.rank.no-data"); + } + + for (int i = 0; i < top.size(); i++) { + final TopElement data = top.get(i); + + if (data.getKey().equals(player.getName())) { + return String.valueOf(i + 1); + } + } + + return lang.getMessage("PLACEHOLDER.rank.unranked"); + } + + if (identifier.startsWith("top")) { + final List top = dataManager.getTopCache(); + + if (top == null) { + return lang.getMessage("PLACEHOLDER.top.loading"); + } + + if (top.isEmpty()) { + return lang.getMessage("PLACEHOLDER.top.no-data"); + } + + final String[] args = identifier.replace("top_", "").split("_"); + final int rank = (int) Math.min(Math.max(1, NumberUtil.parseLong(args[1]).orElse(1)), 10); + + if (rank > top.size()) { + return lang.getMessage("PLACEHOLDER.top.no-data"); + } + + final TopElement element = top.get(rank - 1); + + if (args[0].equals("name")) { + return element.getKey(); + } else if (args[0].equals("tokens")) { + return String.valueOf(element.getTokens()); + } + } + + return null; + } + + @EventHandler + public void on(final PlayerJoinEvent event) { + final Player player = event.getPlayer(); + + if (updateAvailable && (player.isOp() || player.hasPermission(Permissions.CMD_ADMIN))) { + player.sendMessage( + StringUtil.color(String.format(ADMIN_UPDATE_MESSAGE, newVersion, RESOURCE_URL))); + } + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/TokenManager.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/TokenManager.java new file mode 100644 index 00000000..abcf4b0d --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/TokenManager.java @@ -0,0 +1,133 @@ +package me.realized.tokenmanager.api; + +import java.util.Optional; +import java.util.OptionalLong; +import java.util.UUID; +import me.realized.tokenmanager.shop.Shop; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +public interface TokenManager { + + /** + * Get a shop by name. + * + * @param name Shop name + * @return Optional containing the shop instance if found, otherwise empty + */ + Optional getShop(final String name); + + /** + * Get a shop by inventory. + * + * @param inventory inventory to look up for matching shop instance + * @return Optional containing the shop instance if found, otherwise empty + */ + Optional getShop(final Inventory inventory); + + /** + * Get worth of the material defined in worth.yml. + * + * @param material Material to check for worth. + * @return Optional containing the worth of the material if defined. Empty otherwise + * @since v3.2.0 + */ + OptionalLong getWorth(final Material material); + + /** + * Get worth of the item defined in worth.yml. + * + * @param item ItemStack to check for worth. + * @return Optional containing the worth of the item if defined. Empty otherwise + * @since v3.2.0 + */ + OptionalLong getWorth(final ItemStack item); + + /** + * Get online player's token balance. + * + * @param player Player to get token balance + * @return OptionalLong containing token balance if found, otherwise empty + */ + OptionalLong getTokens(final Player player); + + /** + * Set online player's token balance. + * + * @param player Player to set token balance + * @param amount Amount to replace player's token balance + */ + void setTokens(final Player player, final long amount); + + /** + * Add an amount of tokens to online player's token balance. + * + * @param player Player to add tokens + * @param amount Amount of tokens to add + * @return true if add was successful, Otherwise false + * @since v3.2.3 + */ + boolean addTokens(final Player player, final long amount); + + /** + * Remove an amount of tokens from online player's token balance. + * + * @param player Player to remove tokens + * @param amount Amount of tokens to remove + * @return true if remove was successful, Otherwise false + * @since v3.2.3 + */ + boolean removeTokens(final Player player, final long amount); + + /** + * Set player's token balance. + * + * @param key {@link UUID#toString()} if server is in online mode, otherwise name of the player + * @param amount Amount to replace player's token balance + * @since v3.1.0 + */ + void setTokens(final String key, final long amount); + + /** + * Add tokens to player's token balance. + * + * @param key {@link UUID#toString()} if server is in online mode, otherwise name of the player + * @param amount Amount to add to player's token balance + * @param silent true to prevent sending message if target player is online + * @since v3.1.0 + */ + void addTokens(final String key, final long amount, final boolean silent); + + /** + * Works the same as {@link #addTokens(String, long, boolean)} with silent defaulting to false. + * + * @see #addTokens(String, long, boolean) + */ + void addTokens(final String key, final long amount); + + /** + * Remove tokens from player's token balance. + * + * @param key {@link UUID#toString()} if server is in online mode, otherwise name of the player + * @param amount Amount to remove from player's token balance + * @param silent true to prevent sending message if target player is online + * @since v3.1.0 + */ + void removeTokens(final String key, final long amount, final boolean silent); + + /** + * Works the same as {@link #removeTokens(String, long, boolean)} with silent defaulting to false. + * + * @see #removeTokens(String, long, boolean) + */ + void removeTokens(final String key, final long amount); + + /** + * Reload the modules of the plugin. + * + * @return true if reload was successful, otherwise false + */ + boolean reload(); +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMEvent.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMEvent.java new file mode 100644 index 00000000..d8c2c7d2 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMEvent.java @@ -0,0 +1,35 @@ +package me.realized.tokenmanager.api.event; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; + +abstract class TMEvent extends Event implements Cancellable { + + private final Player player; + private final long amount; + private boolean cancelled; + + TMEvent(final Player player, final long amount) { + this.player = player; + this.amount = amount; + } + + public Player getPlayer() { + return player; + } + + public long getAmount() { + return amount; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(final boolean cancelled) { + this.cancelled = cancelled; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMSellAllEvent.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMSellAllEvent.java new file mode 100644 index 00000000..6545cdc0 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMSellAllEvent.java @@ -0,0 +1,22 @@ +package me.realized.tokenmanager.api.event; + +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; + +public class TMSellAllEvent extends TMEvent { + + private static final HandlerList handlers = new HandlerList(); + + public TMSellAllEvent(final Player player, final long amount) { + super(player, amount); + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMSellHandEvent.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMSellHandEvent.java new file mode 100644 index 00000000..7ac46d97 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMSellHandEvent.java @@ -0,0 +1,30 @@ +package me.realized.tokenmanager.api.event; + +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.bukkit.inventory.ItemStack; + +public class TMSellHandEvent extends TMEvent { + + private static final HandlerList handlers = new HandlerList(); + + private final ItemStack item; + + public TMSellHandEvent(final Player player, final long amount, final ItemStack item) { + super(player, amount); + this.item = item; + } + + public ItemStack getItem() { + return item; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMShopPurchaseEvent.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMShopPurchaseEvent.java new file mode 100644 index 00000000..f29d117f --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMShopPurchaseEvent.java @@ -0,0 +1,38 @@ +package me.realized.tokenmanager.api.event; + +import me.realized.tokenmanager.shop.Shop; +import me.realized.tokenmanager.shop.Slot; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; + +public class TMShopPurchaseEvent extends TMEvent { + + private static final HandlerList handlers = new HandlerList(); + + private final Shop shop; + private final Slot slot; + + public TMShopPurchaseEvent( + final Player player, final long amount, final Shop shop, final Slot slot) { + super(player, amount); + this.shop = shop; + this.slot = slot; + } + + public Shop getShop() { + return shop; + } + + public Slot getSlot() { + return slot; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMTokenSendEvent.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMTokenSendEvent.java new file mode 100644 index 00000000..b38beb44 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/api/event/TMTokenSendEvent.java @@ -0,0 +1,29 @@ +package me.realized.tokenmanager.api.event; + +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; + +public class TMTokenSendEvent extends TMEvent { + + private static final HandlerList handlers = new HandlerList(); + + private final Player target; + + public TMTokenSendEvent(final Player player, final Player target, final long amount) { + super(player, amount); + this.target = target; + } + + public Player getTarget() { + return target; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/BaseCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/BaseCommand.java new file mode 100644 index 00000000..5cda5854 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/BaseCommand.java @@ -0,0 +1,74 @@ +package me.realized.tokenmanager.command; + +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.config.Config; +import me.realized.tokenmanager.data.DataManager; +import me.realized.tokenmanager.shop.ShopConfig; +import me.realized.tokenmanager.shop.ShopManager; +import me.realized.tokenmanager.util.command.AbstractCommand; +import me.realized.tokenmanager.util.profile.ProfileUtil; +import org.bukkit.command.CommandSender; + +public abstract class BaseCommand extends AbstractCommand { + + protected final Config config; + protected final ShopConfig shopConfig; + protected final ShopManager shopManager; + protected final DataManager dataManager; + protected final boolean online; + + public BaseCommand( + final TokenManagerPlugin plugin, + final String name, + final String permission, + final boolean playerOnly) { + this(plugin, name, null, permission, 0, playerOnly); + } + + public BaseCommand( + final TokenManagerPlugin plugin, + final String name, + final String usage, + final String permission, + final int length, + final boolean playerOnly, + final String... aliases) { + super(plugin, name, usage, permission, length, playerOnly, aliases); + this.config = plugin.getConfiguration(); + this.shopConfig = plugin.getShopConfig(); + this.shopManager = plugin.getShopManager(); + this.dataManager = plugin.getDataManager(); + + final String mode = config.getOnlineMode(); + this.online = mode.equals("auto") ? ProfileUtil.isOnlineMode() : mode.equals("true"); + } + + protected void sendMessage( + final CommandSender receiver, + final boolean config, + final String in, + final Object... replacers) { + plugin.getLang().sendMessage(receiver, config, in, replacers); + } + + @Override + protected void handleMessage( + final CommandSender sender, final MessageType type, final String... args) { + switch (type) { + case PLAYER_ONLY: + sendMessage(sender, false, "&cThis command can only be executed by a player!"); + break; + case NO_PERMISSION: + sendMessage(sender, true, "ERROR.no-permission", "permission", args[0]); + break; + case SUB_COMMAND_INVALID: + sendMessage( + sender, true, "ERROR.invalid-sub-command", "command", args[0], "input", args[1]); + break; + case SUB_COMMAND_USAGE: + sendMessage( + sender, true, "COMMAND.sub-command-usage", "command", args[0], "usage", args[1]); + break; + } + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/TMCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/TMCommand.java new file mode 100644 index 00000000..59fc80a3 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/TMCommand.java @@ -0,0 +1,32 @@ +package me.realized.tokenmanager.command.commands; + +import me.realized.tokenmanager.Permissions; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.BaseCommand; +import me.realized.tokenmanager.command.commands.subcommands.GiveAllCommand; +import me.realized.tokenmanager.command.commands.subcommands.OfflineCommand; +import me.realized.tokenmanager.command.commands.subcommands.OfflineCommand.ModifyType; +import me.realized.tokenmanager.command.commands.subcommands.OpenCommand; +import me.realized.tokenmanager.command.commands.subcommands.ReloadCommand; +import me.realized.tokenmanager.command.commands.subcommands.TransferCommand; +import org.bukkit.command.CommandSender; + +public class TMCommand extends BaseCommand { + + public TMCommand(final TokenManagerPlugin plugin) { + super(plugin, "tm", Permissions.CMD_ADMIN, false); + child( + new OfflineCommand(plugin, ModifyType.ADD, "give"), + new OfflineCommand(plugin, ModifyType.REMOVE, "delete", "take"), + new OfflineCommand(plugin, ModifyType.SET), + new GiveAllCommand(plugin), + new OpenCommand(plugin), + new TransferCommand(plugin), + new ReloadCommand(plugin)); + } + + @Override + protected void execute(final CommandSender sender, final String label, final String[] args) { + sendMessage(sender, true, "COMMAND.tokenmanager.usage"); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/TokenCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/TokenCommand.java new file mode 100644 index 00000000..bb802333 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/TokenCommand.java @@ -0,0 +1,51 @@ +package me.realized.tokenmanager.command.commands; + +import java.util.OptionalLong; +import me.realized.tokenmanager.Permissions; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.BaseCommand; +import me.realized.tokenmanager.command.commands.subcommands.BalanceCommand; +import me.realized.tokenmanager.command.commands.subcommands.SellCommand; +import me.realized.tokenmanager.command.commands.subcommands.SendCommand; +import me.realized.tokenmanager.command.commands.subcommands.ShopCommand; +import me.realized.tokenmanager.command.commands.subcommands.ShopsCommand; +import me.realized.tokenmanager.command.commands.subcommands.TopCommand; +import me.realized.tokenmanager.command.commands.subcommands.VersionCommand; +import me.realized.tokenmanager.command.commands.subcommands.WorthCommand; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class TokenCommand extends BaseCommand { + + public TokenCommand(final TokenManagerPlugin plugin) { + super(plugin, "token", Permissions.CMD_TOKEN, false); + child( + new BalanceCommand(plugin), + new SendCommand(plugin), + new TopCommand(plugin), + new ShopCommand(plugin), + new ShopsCommand(plugin), + new SellCommand(plugin), + new WorthCommand(plugin), + new VersionCommand(plugin)); + } + + @Override + protected void execute(final CommandSender sender, final String label, final String[] args) { + final OptionalLong balance; + + if (sender instanceof Player) { + final Player player = (Player) sender; + balance = dataManager.get(player); + + if (!balance.isPresent()) { + sendMessage(player, false, "&cYour data is improperly loaded, please re-log."); + return; + } + } else { + balance = OptionalLong.empty(); + } + + sendMessage(sender, true, "COMMAND.token.usage", "tokens", balance.orElse(0)); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/BalanceCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/BalanceCommand.java new file mode 100644 index 00000000..fdc45486 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/BalanceCommand.java @@ -0,0 +1,62 @@ +package me.realized.tokenmanager.command.commands.subcommands; + +import java.util.OptionalLong; +import me.realized.tokenmanager.Permissions; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.BaseCommand; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class BalanceCommand extends BaseCommand { + + public BalanceCommand(final TokenManagerPlugin plugin) { + super(plugin, "balance", "balance", null, 1, false, "bal", "money"); + } + + @Override + protected void execute(final CommandSender sender, final String label, final String[] args) { + final OptionalLong balance; + + if (args.length == getLength()) { + balance = sender instanceof Player ? dataManager.get((Player) sender) : OptionalLong.of(0); + + if (!balance.isPresent()) { + sendMessage(sender, false, "&cFailed to load data of " + sender.getName() + "."); + return; + } + + sendMessage(sender, true, "COMMAND.token.balance", "tokens", balance.getAsLong()); + return; + } + + if (!sender.hasPermission(Permissions.CMD_BALANCE_OTHERS)) { + sendMessage( + sender, true, "ERROR.no-permission", "permission", Permissions.CMD_BALANCE_OTHERS); + return; + } + + final Player target; + + if ((target = Bukkit.getPlayerExact(args[1])) == null) { + sendMessage(sender, true, "ERROR.player-not-found", "input", args[1]); + return; + } + + balance = dataManager.get(target); + + if (!balance.isPresent()) { + sendMessage(sender, true, "ERROR.player-not-found", "input", target.getName()); + return; + } + + sendMessage( + sender, + true, + "COMMAND.token.balance-other", + "player", + target.getName(), + "tokens", + balance.getAsLong()); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/GiveAllCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/GiveAllCommand.java new file mode 100644 index 00000000..3214d535 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/GiveAllCommand.java @@ -0,0 +1,49 @@ +package me.realized.tokenmanager.command.commands.subcommands; + +import java.util.Collection; +import java.util.OptionalLong; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.BaseCommand; +import me.realized.tokenmanager.util.NumberUtil; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class GiveAllCommand extends BaseCommand { + + public GiveAllCommand(final TokenManagerPlugin plugin) { + super(plugin, "giveall", "giveall ", null, 2, false, "sendall"); + } + + @Override + protected void execute(final CommandSender sender, final String label, final String[] args) { + final OptionalLong amount = NumberUtil.parseLong(args[1]); + + if (!amount.isPresent() || amount.getAsLong() <= 0) { + sendMessage(sender, true, "ERROR.invalid-amount", "input", args[1]); + return; + } + + final Collection online = Bukkit.getOnlinePlayers(); + + for (final Player player : online) { + final OptionalLong balance = dataManager.get(player); + + if (!balance.isPresent()) { + continue; + } + + dataManager.set(player, balance.getAsLong() + amount.getAsLong()); + sendMessage(player, true, "COMMAND.add", "amount", amount.getAsLong()); + } + + sendMessage( + sender, + true, + "COMMAND.tokenmanager.giveall", + "players", + online.size(), + "amount", + amount.getAsLong()); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/OfflineCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/OfflineCommand.java new file mode 100644 index 00000000..ad79c56c --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/OfflineCommand.java @@ -0,0 +1,178 @@ +package me.realized.tokenmanager.command.commands.subcommands; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.BaseCommand; +import me.realized.tokenmanager.util.NumberUtil; +import me.realized.tokenmanager.util.profile.ProfileUtil; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class OfflineCommand extends BaseCommand { + + public enum ModifyType { + ADD("COMMAND.tokenmanager.add", (balance, amount) -> balance + amount), + SET("COMMAND.tokenmanager.set", (balance, amount) -> amount), + REMOVE("COMMAND.tokenmanager.remove", (balance, amount) -> balance - amount); + + private final String messageKey; + private final BiFunction action; + + ModifyType(final String messageKey, final BiFunction action) { + this.messageKey = messageKey; + this.action = action; + } + + public String getMessageKey() { + return messageKey; + } + + public long apply(final long balance, final long amount) { + return action.apply(balance, amount); + } + } + + private final ModifyType type; + + public OfflineCommand( + final TokenManagerPlugin plugin, final ModifyType type, final String... aliases) { + super( + plugin, + type.name().toLowerCase(), + type.name().toLowerCase() + " ", + null, + 3, + false, + aliases); + this.type = type; + } + + @Override + protected void execute(final CommandSender sender, final String label, final String[] args) { + final long amount = NumberUtil.parseLong(args[2]).orElse(0); + + if (amount < 0) { + sendMessage(sender, true, "ERROR.invalid-amount", "input", args[2]); + return; + } + + final List options = Arrays.asList(args); + final boolean silent = options.contains("-s"), onlineOnly = options.contains("-o"); + final Player target = Bukkit.getPlayerExact(args[1]); + + // Case: Target player is online + if (target != null) { + sendMessage(sender, true, type.getMessageKey(), "amount", amount, "player", target.getName()); + + if (type == ModifyType.SET) { + dataManager.set(target, amount); + return; + } else { + final OptionalLong balance = dataManager.get(target); + + if (!balance.isPresent()) { + dataManager.queueCommand(target, type, amount, silent); + sendMessage( + sender, + false, + "&c" + + target.getName() + + "'s data is currently loading! Command has been queued for future execution."); + return; + } + + dataManager.set(target, type.apply(balance.getAsLong(), amount)); + } + + if (!silent) { + plugin + .getLang() + .sendMessage( + target, + true, + "COMMAND." + (type == ModifyType.ADD ? "add" : "remove"), + "amount", + amount); + } + + return; + } else if (onlineOnly) { + sendMessage(sender, true, "ERROR.player-not-found", "input", args[1]); + return; + } + + // UUID conversion if server is online mode + getTarget( + sender, + args[1], + key -> { + if (!key.isPresent()) { + sendMessage(sender, true, "ERROR.player-not-found", "input", args[1]); + return; + } + + dataManager.get( + key.get(), + balance -> { + // Case: Not found in the database + if (!balance.isPresent()) { + sendMessage(sender, true, "ERROR.player-not-found", "input", args[1]); + return; + } + + dataManager.set( + key.get(), + type, + amount, + type.apply(balance.getAsLong(), amount), + silent, + () -> + sendMessage( + sender, + true, + type.getMessageKey(), + "amount", + amount, + "player", + args[1]), + error -> + sendMessage( + sender, + false, + "&cThere was an error while executing this command, please contact an administrator.")); + }, + error -> + sender.sendMessage( + ChatColor.RED + + "Could not get token balance of " + + key.get() + + ": " + + error)); + }); + } + + private void getTarget( + final CommandSender sender, final String input, final Consumer> consumer) { + // Case: Server is in offline mode. + if (!online) { + consumer.accept(Optional.of(input)); + return; + } + + plugin.doAsync( + () -> + ProfileUtil.getUUID( + input, + uuid -> consumer.accept(Optional.ofNullable(uuid)), + error -> + sender.sendMessage( + ChatColor.RED + "Failed to obtain UUID of " + input + ": " + error))); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/OpenCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/OpenCommand.java new file mode 100644 index 00000000..e796ef7d --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/OpenCommand.java @@ -0,0 +1,55 @@ +package me.realized.tokenmanager.command.commands.subcommands; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.BaseCommand; +import me.realized.tokenmanager.shop.Shop; +import me.realized.tokenmanager.shop.gui.guis.ShopGui; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class OpenCommand extends BaseCommand { + + public OpenCommand(final TokenManagerPlugin plugin) { + super(plugin, "open", "open ", null, 3, false, "show"); + } + + @Override + protected void execute(final CommandSender sender, final String label, final String[] args) { + final Player target; + + if ((target = Bukkit.getPlayerExact(args[1])) == null) { + sendMessage(sender, true, "ERROR.player-not-found", "input", args[1]); + return; + } + + final String name = args[2].toLowerCase(); + final Optional shop = shopConfig.getShop(name); + + if (!shop.isPresent()) { + sendMessage(sender, true, "ERROR.shop-not-found", "input", name); + return; + } + + shopManager.open(target, new ShopGui(plugin, shop.get())); + sendMessage( + sender, true, "COMMAND.tokenmanager.open", "name", name, "player", target.getName()); + } + + @Override + public List onTabComplete( + final CommandSender sender, final Command command, final String alias, final String[] args) { + if (args.length == 3) { + return shopConfig.getShops().stream() + .map(Shop::getName) + .filter(name -> name.toLowerCase().startsWith(args[2].toLowerCase())) + .collect(Collectors.toList()); + } + + return null; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/ReloadCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/ReloadCommand.java new file mode 100644 index 00000000..60903786 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/ReloadCommand.java @@ -0,0 +1,75 @@ +package me.realized.tokenmanager.command.commands.subcommands; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.BaseCommand; +import me.realized.tokenmanager.util.Loadable; +import me.realized.tokenmanager.util.Reloadable; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; + +public class ReloadCommand extends BaseCommand { + + public ReloadCommand(final TokenManagerPlugin plugin) { + super(plugin, "reload", "reload", null, 1, false, "rl"); + } + + @Override + protected void execute(final CommandSender sender, final String label, final String[] args) { + if (args.length > getLength()) { + final Optional target = plugin.find(args[1]); + final Loadable loadable; + + if (!target.isPresent() || !((loadable = target.get()) instanceof Reloadable)) { + sendMessage(sender, false, "&cInvalid module. Available: " + plugin.getReloadables()); + return; + } + + final String name = loadable.getClass().getSimpleName(); + + if (plugin.reload(loadable)) { + sendMessage( + sender, + false, + "&a[" + + plugin.getDescription().getFullName() + + "] Successfully reloaded " + + name + + "."); + } else { + sendMessage( + sender, + false, + "&cAn error occured while reloading " + + name + + "! Please check the console for more information."); + } + + return; + } + + if (plugin.reload()) { + sendMessage( + sender, false, "&a[" + plugin.getDescription().getFullName() + "] Reload complete."); + } else { + sendMessage( + sender, + false, + "&cAn error occured while reloading the plugin! The plugin will be disabled, please check the console for more information."); + } + } + + @Override + public List onTabComplete( + final CommandSender sender, final Command command, final String alias, final String[] args) { + if (args.length == 2) { + return plugin.getReloadables().stream() + .filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase())) + .collect(Collectors.toList()); + } + + return null; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/SellCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/SellCommand.java new file mode 100644 index 00000000..e8cf9090 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/SellCommand.java @@ -0,0 +1,115 @@ +package me.realized.tokenmanager.command.commands.subcommands; + +import java.util.OptionalLong; +import me.realized.tokenmanager.Permissions; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.api.event.TMSellAllEvent; +import me.realized.tokenmanager.api.event.TMSellHandEvent; +import me.realized.tokenmanager.command.BaseCommand; +import org.apache.commons.lang3.text.WordUtils; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; + +public class SellCommand extends BaseCommand { + + public SellCommand(final TokenManagerPlugin plugin) { + super(plugin, "sell", "sell", Permissions.CMD_SELL, 1, true); + } + + @Override + protected void execute(final CommandSender sender, final String label, final String[] args) { + final Player player = (Player) sender; + + if (args.length > getLength() && args[1].equalsIgnoreCase("all")) { + if (!player.hasPermission(Permissions.CMD_SELL_ALL)) { + sendMessage(sender, true, "ERROR.no-permission", "permission", Permissions.CMD_SELL_ALL); + return; + } + + final PlayerInventory inventory = player.getInventory(); + int total = 0; + long price = 0; + + for (int slot = 0; slot < 36; slot++) { + final ItemStack item = inventory.getItem(slot); + + if (item == null) { + continue; + } + + final OptionalLong worth = plugin.getWorth(item); + + if (worth.isPresent()) { + price += worth.getAsLong(); + total += item.getAmount(); + } + } + + final TMSellAllEvent event = new TMSellAllEvent(player, price); + plugin.getServer().getPluginManager().callEvent(event); + + if (event.isCancelled()) { + return; + } + + for (int slot = 0; slot < 36; slot++) { + final ItemStack item = inventory.getItem(slot); + + if (item == null) { + continue; + } + + final OptionalLong worth = plugin.getWorth(item); + + if (worth.isPresent()) { + inventory.setItem(slot, null); + } + } + + dataManager.set(player, dataManager.get(player).orElse(0) + price); + sendMessage(sender, true, "COMMAND.token.sell-all", "item_amount", total, "amount", price); + return; + } + + final int heldSlot = player.getInventory().getHeldItemSlot(); + final ItemStack item = player.getInventory().getItem(heldSlot); + + if (item == null) { + sendMessage(sender, true, "ERROR.no-item-in-hand"); + return; + } + + final OptionalLong worth = plugin.getWorth(item); + + if (!worth.isPresent()) { + sendMessage(sender, true, "ERROR.item-is-worthless"); + return; + } + + final long price = worth.getAsLong(); + final TMSellHandEvent event = new TMSellHandEvent(player, price, item); + plugin.getServer().getPluginManager().callEvent(event); + + if (event.isCancelled()) { + return; + } + + player.getInventory().setItem(heldSlot, null); + dataManager.set(player, dataManager.get(player).orElse(0) + price); + + final String name = + WordUtils.capitalizeFully(item.getType().toString().replace("_", " ").toLowerCase()); + sendMessage( + sender, + true, + "COMMAND.token.sell", + "item_type", + name, + "item_amount", + item.getAmount(), + "amount", + price); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/SendCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/SendCommand.java new file mode 100644 index 00000000..ee790deb --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/SendCommand.java @@ -0,0 +1,109 @@ +package me.realized.tokenmanager.command.commands.subcommands; + +import java.util.Arrays; +import java.util.List; +import java.util.OptionalLong; +import me.realized.tokenmanager.Permissions; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.api.event.TMTokenSendEvent; +import me.realized.tokenmanager.command.BaseCommand; +import me.realized.tokenmanager.event.TokenReceiveEvent; +import me.realized.tokenmanager.util.NumberUtil; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class SendCommand extends BaseCommand { + + private static final List TAB_AMOUNTS = + Arrays.asList("5", "10", "25", "50", "75", "100", "500", "1000"); + + public SendCommand(final TokenManagerPlugin plugin) { + super(plugin, "send", "send ", Permissions.CMD_SEND, 3, true); + } + + @Override + protected void execute(final CommandSender sender, final String label, final String[] args) { + final Player target; + + if ((target = Bukkit.getPlayerExact(args[1])) == null) { + sendMessage(sender, true, "ERROR.player-not-found", "input", args[1]); + return; + } + + if (target.equals(sender)) { + sendMessage(sender, true, "ERROR.target-is-self"); + return; + } + + if (config.isAltPrevention() + && target.getAddress().getHostName().equals(((Player) sender).getAddress().getHostName())) { + sendMessage(sender, true, "ERROR.target-has-same-ip"); + return; + } + + final OptionalLong targetBalance = dataManager.get(target); + + if (!targetBalance.isPresent()) { + sendMessage(sender, false, "&cFailed to load data of " + target.getName() + "."); + return; + } + + final long amount = NumberUtil.parseLong(args[2]).orElse(0); + + if (amount <= 0 + || (config.getSendMin() > -1 && amount < config.getSendMin()) + || (config.getSendMax() > -1 && amount > config.getSendMax())) { + sendMessage(sender, true, "ERROR.invalid-amount", "input", args[2]); + return; + } + + final Player player = (Player) sender; + final OptionalLong balance = dataManager.get(player); + + if (!balance.isPresent()) { + sendMessage(sender, true, "&cFailed to load data of " + sender.getName() + "."); + return; + } + + final long needed; + + if ((needed = balance.getAsLong() - amount) < 0) { + sendMessage(sender, true, "ERROR.balance-not-enough", "needed", Math.abs(needed)); + return; + } + + final TMTokenSendEvent event = new TMTokenSendEvent(player, target, amount); + plugin.getServer().getPluginManager().callEvent(event); + + if (event.isCancelled()) { + return; + } + + dataManager.set(player, balance.getAsLong() - amount); + sendMessage(sender, true, "COMMAND.token.send", "player", target.getName(), "amount", amount); + + final TokenReceiveEvent tokenReceiveEvent = + new TokenReceiveEvent(target.getUniqueId(), (int) amount); + Bukkit.getPluginManager().callEvent(tokenReceiveEvent); + + if (tokenReceiveEvent.isCancelled()) { + return; + } + + dataManager.set(target, targetBalance.getAsLong() + amount); + sendMessage( + target, true, "COMMAND.token.receive", "player", sender.getName(), "amount", amount); + } + + @Override + public List onTabComplete( + final CommandSender sender, final Command command, final String label, final String[] args) { + if (args.length == 3) { + return TAB_AMOUNTS; + } + + return null; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/ShopCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/ShopCommand.java new file mode 100644 index 00000000..6edc6e8b --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/ShopCommand.java @@ -0,0 +1,74 @@ +package me.realized.tokenmanager.command.commands.subcommands; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import me.realized.tokenmanager.Permissions; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.BaseCommand; +import me.realized.tokenmanager.shop.Shop; +import me.realized.tokenmanager.shop.gui.guis.ShopGui; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class ShopCommand extends BaseCommand { + + public ShopCommand(final TokenManagerPlugin plugin) { + super( + plugin, + "shop", + "shop ", + Permissions.CMD_SHOP, + plugin.getConfiguration().isOpenSelectedEnabled() ? 1 : 2, + true); + } + + @Override + protected void execute(final CommandSender sender, final String label, final String[] args) { + final Player player = (Player) sender; + final String target; + final Optional shop; + + if (config.isOpenSelectedEnabled()) { + target = config.getOpenSelectedShop(); + shop = shopConfig.getShop(target); + + if (!shop.isPresent()) { + sendMessage(sender, true, "ERROR.shop-not-found", "input", target); + return; + } + + shopManager.open(player, new ShopGui(plugin, shop.get())); + return; + } + + target = args[1].toLowerCase(); + shop = shopConfig.getShop(target); + + if (!shop.isPresent()) { + sendMessage(player, true, "ERROR.shop-not-found", "input", target); + return; + } + + if (shop.get().isUsePermission() && !player.hasPermission(Permissions.SHOP + target)) { + sendMessage(player, true, "ERROR.no-permission", "permission", Permissions.SHOP + target); + return; + } + + shopManager.open(player, new ShopGui(plugin, shop.get())); + } + + @Override + public List onTabComplete( + final CommandSender sender, final Command command, final String alias, final String[] args) { + if (args.length == 2 && !config.isOpenSelectedEnabled()) { + return shopConfig.getShops().stream() + .map(Shop::getName) + .filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase())) + .collect(Collectors.toList()); + } + + return null; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/ShopsCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/ShopsCommand.java new file mode 100644 index 00000000..14c792d2 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/ShopsCommand.java @@ -0,0 +1,27 @@ +package me.realized.tokenmanager.command.commands.subcommands; + +import java.util.stream.Collectors; +import me.realized.tokenmanager.Permissions; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.BaseCommand; +import me.realized.tokenmanager.shop.Shop; +import org.apache.commons.lang3.StringUtils; +import org.bukkit.command.CommandSender; + +public class ShopsCommand extends BaseCommand { + + public ShopsCommand(final TokenManagerPlugin plugin) { + super(plugin, "shops", "shops", Permissions.CMD_SHOP, 1, false); + } + + @Override + protected void execute(final CommandSender sender, final String label, final String[] args) { + sendMessage( + sender, + true, + "COMMAND.token.shops", + "shops", + StringUtils.join( + shopConfig.getShops().stream().map(Shop::getName).collect(Collectors.toList()), ", ")); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/TopCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/TopCommand.java new file mode 100644 index 00000000..65860bd2 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/TopCommand.java @@ -0,0 +1,50 @@ +package me.realized.tokenmanager.command.commands.subcommands; + +import java.util.List; +import me.realized.tokenmanager.Permissions; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.BaseCommand; +import me.realized.tokenmanager.data.database.Database.TopElement; +import org.bukkit.command.CommandSender; + +public class TopCommand extends BaseCommand { + + public TopCommand(final TokenManagerPlugin plugin) { + super(plugin, "top", "top", Permissions.CMD_TOP, 1, false, "balancetop"); + } + + @Override + protected void execute(final CommandSender sender, final String label, final String[] args) { + sendMessage( + sender, + true, + "COMMAND.token.balance-top.next-update", + "remaining", + dataManager.getNextUpdate()); + + final List top = dataManager.getTopCache(); + + sendMessage( + sender, true, "COMMAND.token.balance-top.header", "total", (top != null ? top.size() : 0)); + + if (top == null || top.isEmpty()) { + sendMessage(sender, true, "ERROR.data-not-enough"); + } else { + for (int i = 0; i < top.size(); i++) { + final TopElement element = top.get(i); + sendMessage( + sender, + true, + "COMMAND.token.balance-top.display-format", + "rank", + i + 1, + "name", + element.getKey(), + "tokens", + element.getTokens()); + } + } + + sendMessage(sender, true, "COMMAND.token.balance-top.footer"); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/TransferCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/TransferCommand.java new file mode 100644 index 00000000..fdea3dfa --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/TransferCommand.java @@ -0,0 +1,19 @@ +package me.realized.tokenmanager.command.commands.subcommands; + +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.BaseCommand; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +public class TransferCommand extends BaseCommand { + + public TransferCommand(final TokenManagerPlugin plugin) { + super(plugin, "transfer", "transfer", null, 1, false); + } + + @Override + protected void execute(final CommandSender sender, final String label, final String[] args) { + dataManager.transfer( + sender, error -> sender.sendMessage(ChatColor.RED + "Could not transfer data: " + error)); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/VersionCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/VersionCommand.java new file mode 100644 index 00000000..91b1900e --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/VersionCommand.java @@ -0,0 +1,23 @@ +package me.realized.tokenmanager.command.commands.subcommands; + +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.BaseCommand; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +public class VersionCommand extends BaseCommand { + + public VersionCommand(final TokenManagerPlugin plugin) { + super(plugin, "version", "version", null, 1, false, "v"); + } + + @Override + protected void execute(final CommandSender sender, final String label, final String[] args) { + sender.sendMessage( + ChatColor.AQUA + + plugin.getDescription().getFullName() + + " by " + + plugin.getDescription().getAuthors().get(0)); + sender.sendMessage(ChatColor.AQUA + "Download: " + plugin.getDescription().getWebsite()); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/WorthCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/WorthCommand.java new file mode 100644 index 00000000..89c08365 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/command/commands/subcommands/WorthCommand.java @@ -0,0 +1,94 @@ +package me.realized.tokenmanager.command.commands.subcommands; + +import java.util.OptionalLong; +import me.realized.tokenmanager.Permissions; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.BaseCommand; +import org.apache.commons.lang3.text.WordUtils; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; + +public class WorthCommand extends BaseCommand { + + public WorthCommand(final TokenManagerPlugin plugin) { + super(plugin, "worth", "worth", Permissions.CMD_WORTH, 1, true); + } + + @Override + protected void execute(final CommandSender sender, final String label, final String[] args) { + final Player player = (Player) sender; + + if (args.length > getLength() && args[1].equalsIgnoreCase("all")) { + if (!player.hasPermission(Permissions.CMD_WORTH_ALL)) { + sendMessage(sender, true, "ERROR.no-permission", "permission", Permissions.CMD_WORTH_ALL); + return; + } + + final PlayerInventory inventory = player.getInventory(); + int total = 0; + long price = 0; + + for (int slot = 0; slot < 36; slot++) { + final ItemStack item = inventory.getItem(slot); + + if (item == null) { + continue; + } + + final OptionalLong worth = plugin.getWorth(item); + + if (worth.isPresent()) { + price += worth.getAsLong(); + total += item.getAmount(); + + final String name = + WordUtils.capitalizeFully(item.getType().toString().replace("_", " ").toLowerCase()); + sendMessage( + sender, + true, + "COMMAND.token.worth-all.item-format", + "item_type", + name, + "item_amount", + item.getAmount(), + "amount", + price); + } + } + + sendMessage( + sender, true, "COMMAND.token.worth-all.total", "item_amount", total, "amount", price); + return; + } + + final ItemStack item = player.getInventory().getItem(player.getInventory().getHeldItemSlot()); + + if (item == null) { + sendMessage(sender, true, "ERROR.no-item-in-hand"); + return; + } + + final OptionalLong worth = plugin.getWorth(item); + + if (!worth.isPresent()) { + sendMessage(sender, true, "ERROR.item-is-worthless"); + return; + } + + final long price = worth.getAsLong(); + final String name = + WordUtils.capitalizeFully(item.getType().toString().replace("_", " ").toLowerCase()); + sendMessage( + sender, + true, + "COMMAND.token.worth", + "item_type", + name, + "item_amount", + item.getAmount(), + "amount", + price); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/config/Config.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/config/Config.java new file mode 100644 index 00000000..4b06bfdb --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/config/Config.java @@ -0,0 +1,114 @@ +package me.realized.tokenmanager.config; + +import java.util.HashMap; +import java.util.Map; +import lombok.Getter; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.util.config.AbstractConfiguration; +import me.realized.tokenmanager.util.config.convert.Converter; +import org.bukkit.configuration.file.FileConfiguration; + +public class Config extends AbstractConfiguration { + + @Getter private int version; + @Getter private boolean checkForUpdates; + @Getter private String onlineMode; + @Getter private boolean altPrevention; + @Getter private int defaultBalance; + @Getter private long sendMin; + @Getter private long sendMax; + @Getter private boolean openSelectedEnabled; + @Getter private String openSelectedShop; + @Getter private String confirmPurchaseTitle; + @Getter private String confirmPurchaseConfirm; + @Getter private String confirmPurchaseCancel; + @Getter private int clickDelay; + @Getter private boolean checkInventoryFull; + @Getter private boolean logPurchases; + @Getter private boolean mysqlEnabled; + @Getter private String mysqlUsername; + @Getter private String mysqlPassword; + @Getter private String mysqlHostname; + @Getter private String mysqlPort; + @Getter private String mysqlDatabase; + @Getter private String mysqlTable; + @Getter private String mysqlUrl; + @Getter private boolean redisEnabled; + @Getter private String redisServer; + @Getter private int redisPort; + @Getter private String redisPassword; + @Getter private boolean registerEconomy; + @Getter private int balanceTopUpdateInterval; + + public Config(final TokenManagerPlugin plugin) { + super(plugin, "config"); + } + + @Override + protected void loadValues(FileConfiguration configuration) throws Exception { + if (!configuration.isInt("config-version")) { + configuration = convert(new Converter2_3()); + } else if (configuration.getInt("config-version") < getLatestVersion()) { + configuration = convert(null); + } + + version = configuration.getInt("config-version"); + checkForUpdates = configuration.getBoolean("check-for-updates", true); + onlineMode = configuration.getString("online-mode", "auto"); + altPrevention = configuration.getBoolean("alt-prevention", false); + defaultBalance = configuration.getInt("default-balance", 25); + sendMin = configuration.getInt("send-amount-limit.min", 1); + sendMax = configuration.getInt("send-amount-limit.max", -1); + openSelectedEnabled = configuration.getBoolean("shop.open-selected.enabled", false); + openSelectedShop = configuration.getString("shop.open-selected.shop", "example").toLowerCase(); + confirmPurchaseTitle = + configuration.getString("shop.confirm-purchase-gui.title", "Confirm Your Purchase"); + confirmPurchaseConfirm = + configuration.getString( + "shop.confirm-purchase-gui.confirm-button", + "STAINED_CLAY:5 1 name:&a&lBUY lore:&7Price:_&a%price%_tokens"); + confirmPurchaseCancel = + configuration.getString( + "shop.confirm-purchase-gui.cancel-button", "STAINED_CLAY:14 1 name:&c&lCANCEL"); + clickDelay = configuration.getInt("shop.click-delay", 0); + checkInventoryFull = configuration.getBoolean("shop.check-inventory-full", false); + logPurchases = configuration.getBoolean("shop.log-purchases", false); + mysqlEnabled = configuration.getBoolean("data.mysql.enabled", false); + mysqlUsername = configuration.getString("data.mysql.username", "root"); + mysqlPassword = configuration.getString("data.mysql.password", "password"); + mysqlHostname = configuration.getString("data.mysql.hostname", "127.0.0.1"); + mysqlPort = configuration.getString("data.mysql.port", "3306"); + mysqlDatabase = configuration.getString("data.mysql.database", "database"); + mysqlTable = configuration.getString("data.mysql.table", "tokenmanager"); + mysqlUrl = + configuration.getString("data.mysql.url", "jdbc:mysql://%hostname%:%port%/%database%"); + redisEnabled = configuration.getBoolean("data.mysql.redis.enabled", false); + redisServer = configuration.getString("data.mysql.redis.server", "127.0.0.1"); + redisPort = configuration.getInt("data.mysql.redis.port", 6379); + redisPassword = configuration.getString("data.mysql.redis.password", ""); + registerEconomy = configuration.getBoolean("data.register-economy", false); + balanceTopUpdateInterval = configuration.getInt("data.balance-top-update-interval", 5); + } + + private class Converter2_3 implements Converter { + + Converter2_3() {} + + @Override + public Map renamedKeys() { + final Map keys = new HashMap<>(); + keys.put("use-default.enabled", "shop.open-selected.enabled"); + keys.put("use-default.shop", "shop.open-selected.shop"); + keys.put("mysql.enabled", "data.mysql.enabled"); + keys.put("mysql.hostname", "data.mysql.hostname"); + keys.put("mysql.port", "data.mysql.port"); + keys.put("mysql.username", "data.mysql.username"); + keys.put("mysql.password", "data.mysql.password"); + keys.put("mysql.database", "data.mysql.database"); + keys.put("click-delay", "shop.click-delay"); + keys.put("update-balance-top", "data.balance-top-update-interval"); + keys.put("vault-hooks", "data.register-economy"); + return keys; + } + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/config/Lang.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/config/Lang.java new file mode 100644 index 00000000..ddf3206a --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/config/Lang.java @@ -0,0 +1,200 @@ +package me.realized.tokenmanager.config; + +import com.google.common.collect.Sets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.util.Log; +import me.realized.tokenmanager.util.Placeholders; +import me.realized.tokenmanager.util.Reloadable; +import me.realized.tokenmanager.util.StringUtil; +import me.realized.tokenmanager.util.config.AbstractConfiguration; +import me.realized.tokenmanager.util.config.convert.Converter; +import org.apache.commons.lang3.StringUtils; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.MemorySection; +import org.bukkit.configuration.file.FileConfiguration; + +public class Lang extends AbstractConfiguration implements Reloadable { + + private final Map messages = new HashMap<>(); + + public Lang(final TokenManagerPlugin plugin) { + super(plugin, "lang"); + } + + @Override + protected void loadValues(FileConfiguration configuration) throws Exception { + if (!configuration.isInt("config-version")) { + configuration = convert(new Converter2_3()); + } else if (configuration.getInt("config-version") < getLatestVersion()) { + configuration = convert(null); + } + + final Map strings = new HashMap<>(); + + for (String key : configuration.getKeys(true)) { + if (key.equals("config-version")) { + continue; + } + + // Fixes a weird occurrence with FileConfiguration#getKeys that an extra separator char is + // prepended when called after FileConfiguration#set + if (key.startsWith(".")) { + key = key.substring(1); + } + + final Object value = configuration.get(key); + + if (value == null || value instanceof MemorySection) { + continue; + } + + final String message = + value instanceof List ? StringUtil.fromList((List) value) : value.toString(); + + if (key.startsWith("STRINGS")) { + final String[] args = key.split(Pattern.quote(".")); + strings.put(args[args.length - 1], message); + } else { + messages.put(key, message); + } + } + + messages.replaceAll( + (key, value) -> { + for (final Map.Entry entry : strings.entrySet()) { + final String placeholder = "{" + entry.getKey() + "}"; + + if (StringUtils.containsIgnoreCase(value, placeholder)) { + value = value.replaceAll("(?i)" + Pattern.quote(placeholder), entry.getValue()); + } + } + + return value; + }); + } + + @Override + public void handleUnload() { + messages.clear(); + } + + @Override + protected Set transferredSections() { + return Sets.newHashSet("STRINGS"); + } + + private String getRawMessage(final String key) { + final String message = messages.get(key); + + if (message == null) { + Log.error(this, "Failed to load message: provided key '" + key + "' has no assigned value"); + return null; + } + + // Allow disabling any message by setting it to '' + if (message.isEmpty()) { + return null; + } + + return message; + } + + public String getMessage(final String key) { + final String message = getRawMessage(key); + + if (message == null) { + return null; + } + + return StringUtil.color(message); + } + + public String getMessage(final String key, final Object... replacers) { + final String message = getMessage(key); + + if (message == null) { + return null; + } + + return replace(message, replacers); + } + + public void sendMessage( + final CommandSender receiver, + final boolean config, + final String in, + final Object... replacers) { + if (config) { + String message = getRawMessage(in); + + if (message == null) { + return; + } + + receiver.sendMessage(StringUtil.color(replace(message, replacers))); + } else { + receiver.sendMessage(StringUtil.color(replace(in, replacers))); + } + } + + private String replace(String message, final Object... replacers) { + for (int i = 0; i < replacers.length; i += 2) { + if (i + 1 >= replacers.length) { + break; + } + + final String key = String.valueOf(replacers[i]); + final Object value = replacers[i + 1]; + + if (value instanceof Long) { + message = Placeholders.replace(message, (Long) value, key); + } + + message = message.replace("%" + key + "%", String.valueOf(value)); + } + + return message; + } + + private static class Converter2_3 implements Converter { + + Converter2_3() {} + + @Override + public Map renamedKeys() { + final Map keys = new HashMap<>(); + keys.put("no-permission", "ERROR.no-permission"); + keys.put("invalid-amount", "ERROR.invalid-amount"); + keys.put("invalid-player", "ERROR.player-not-found"); + keys.put("invalid-shop", "ERROR.shop-not-found"); + keys.put("invalid-sub-command", "ERROR.invalid-sub-command"); + keys.put("not-enough-tokens", "ERROR.balance-not-enough"); + keys.put("no-data", "ERROR.data-not-enough"); + keys.put("click-spamming", "ERROR.on-click-cooldown"); + keys.put("token-help-page", "COMMAND.token.usage"); + keys.put("tm-help-page", "COMMAND.tokenmanager.usage"); + keys.put("sub-command-usage", "COMMAND.sub-command-usage"); + keys.put("top-next-update", "COMMAND.token.balance-top.next-update"); + keys.put("top-header", "COMMAND.token.balance-top.header"); + keys.put("top-format", "COMMAND.token.balance-top.display-format"); + keys.put("top-footer", "COMMAND.token.balance-top.footer"); + keys.put("balance", "COMMAND.token.balance"); + keys.put("balance-others", "COMMAND.token.balance-other"); + keys.put("on-send", "COMMAND.token.send"); + keys.put("on-receive", "COMMAND.add"); + keys.put("on-take", "COMMAND.remove"); + keys.put("on-add", "COMMAND.tokenmanager.add"); + keys.put("on-remove", "COMMAND.tokenmanager.remove"); + keys.put("on-set", "COMMAND.tokenmanager.set"); + keys.put("on-open", "COMMAND.tokenmanager.open"); + keys.put("shops", "COMMAND.token.shops"); + keys.put("on-give-all", "COMMAND.tokenmanager.giveall"); + return keys; + } + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/config/WorthConfig.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/config/WorthConfig.java new file mode 100644 index 00000000..c744c372 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/config/WorthConfig.java @@ -0,0 +1,105 @@ +package me.realized.tokenmanager.config; + +import java.util.HashMap; +import java.util.Map; +import java.util.OptionalLong; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.util.Reloadable; +import me.realized.tokenmanager.util.config.AbstractConfiguration; +import org.bukkit.Material; +import org.bukkit.configuration.MemorySection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.inventory.ItemStack; + +public class WorthConfig extends AbstractConfiguration implements Reloadable { + + private final Map worth = new HashMap<>(); + + public WorthConfig(final TokenManagerPlugin plugin) { + super(plugin, "worth"); + } + + @Override + protected void loadValues(final FileConfiguration configuration) { + configuration + .getKeys(false) + .forEach( + key -> { + final Material type = Material.getMaterial(key); + + if (type == null) { + return; + } + + final Object value = configuration.get(key); + + if (value instanceof Number) { + worth.put(type, new WorthData((long) ((int) value))); + return; + } else if (!(value instanceof MemorySection)) { + return; + } + + final MemorySection section = (MemorySection) value; + final WorthData data = new WorthData(); + section + .getKeys(false) + .forEach( + durability -> { + if (durability.equals("*")) { + data.baseWorthSet = true; + data.baseWorth = section.getLong(durability); + return; + } + + data.extraWorth.put(Short.valueOf(durability), section.getLong(durability)); + }); + worth.put(type, data); + }); + } + + @Override + public void handleUnload() { + worth.clear(); + } + + public OptionalLong getWorth(final Material material) { + final WorthData data; + return ((data = worth.get(material)) != null && data.baseWorthSet) + ? OptionalLong.of(data.baseWorth) + : OptionalLong.empty(); + } + + public OptionalLong getWorth(final ItemStack item) { + final WorthData data; + return (data = worth.get(item.getType())) != null ? data.worthOf(item) : OptionalLong.empty(); + } + + private static class WorthData { + + private boolean baseWorthSet; + private long baseWorth; + private Map extraWorth; + + WorthData(final long baseWorth) { + this.baseWorth = baseWorth; + this.baseWorthSet = true; + } + + WorthData() { + extraWorth = new HashMap<>(); + } + + OptionalLong worthOf(final ItemStack item) { + if (extraWorth != null) { + final Long value = extraWorth.get(item.getDurability()); + + if (value != null) { + return OptionalLong.of(value * item.getAmount()); + } + } + + return baseWorthSet ? OptionalLong.of(baseWorth * item.getAmount()) : OptionalLong.empty(); + } + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/DataManager.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/DataManager.java new file mode 100644 index 00000000..c3e3d6a9 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/DataManager.java @@ -0,0 +1,216 @@ +package me.realized.tokenmanager.data; + +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.OptionalLong; +import java.util.UUID; +import java.util.function.Consumer; +import lombok.Getter; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.commands.subcommands.OfflineCommand.ModifyType; +import me.realized.tokenmanager.data.database.Database; +import me.realized.tokenmanager.data.database.Database.TopElement; +import me.realized.tokenmanager.data.database.FileDatabase; +import me.realized.tokenmanager.data.database.MySQLDatabase; +import me.realized.tokenmanager.util.Loadable; +import me.realized.tokenmanager.util.Log; +import me.realized.tokenmanager.util.StringUtil; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.scheduler.BukkitScheduler; + +public class DataManager implements Loadable, Listener { + + private final TokenManagerPlugin plugin; + + private Database database; + + @Getter private List topCache = new ArrayList<>(); + private Integer topTask, updateInterval; + private long lastUpdateMillis; + + private final Multimap queuedCommands = LinkedHashMultimap.create(); + + public DataManager(final TokenManagerPlugin plugin) { + this.plugin = plugin; + Bukkit.getPluginManager().registerEvents(this, plugin); + } + + @Override + public void handleLoad() throws Exception { + this.database = + plugin.getConfiguration().isMysqlEnabled() + ? new MySQLDatabase(plugin) + : new FileDatabase(plugin); + final boolean online = database.isOnlineMode(); + Log.info("==============================================="); + Log.info( + "TokenManager has detected your server as " + (online ? "online" : "offline") + " mode."); + Log.info("DataManager will operate with " + (online ? "UUID" : "Username") + "s."); + Log.info( + "If your server is NOT in " + + (online ? "online" : "offline") + + " mode, please manually set online-mode in TokenManager's config.yml."); + Log.info("==============================================="); + database.setup(); + + topTask = + plugin.doSyncRepeat( + () -> + database.ordered( + 10, + args -> + plugin.doSync( + () -> { + lastUpdateMillis = System.currentTimeMillis(); + topCache = args; + })), + 0L, + 20L * 60L * getUpdateInterval()); + + Bukkit.getOnlinePlayers().forEach(player -> database.load(player)); + } + + @Override + public void handleUnload() throws Exception { + if (topTask != null) { + final BukkitScheduler scheduler = Bukkit.getScheduler(); + + if (scheduler.isCurrentlyRunning(topTask) || scheduler.isQueued(topTask)) { + scheduler.cancelTask(topTask); + } + } + + database.shutdown(); + database = null; + } + + public OptionalLong get(final Player player) { + return database != null ? database.get(player) : OptionalLong.empty(); + } + + public void set(final Player player, final long amount) { + if (database != null) { + database.set(player, amount); + } + } + + public void get( + final String key, final Consumer onLoad, final Consumer onError) { + if (database != null) { + database.get(key, onLoad, onError, false); + } + } + + public void set( + final String key, + final ModifyType type, + final long amount, + final long balance, + final boolean silent, + final Runnable onDone, + final Consumer onError) { + if (database != null) { + database.set(key, type, amount, balance, silent, onDone, onError); + } + } + + public void transfer(final CommandSender sender, final Consumer onError) { + if (database != null) { + database.transfer(sender, onError); + } + } + + public void queueCommand( + final Player player, final ModifyType type, final long amount, final boolean silent) { + queuedCommands.put(player.getUniqueId(), new QueuedCommand(type, amount, silent)); + } + + private int getUpdateInterval() { + if (updateInterval != null) { + return updateInterval; + } + + return (updateInterval = plugin.getConfiguration().getBalanceTopUpdateInterval()) < 1 + ? 1 + : updateInterval; + } + + public String getNextUpdate() { + return StringUtil.format( + (lastUpdateMillis + 60000L * getUpdateInterval() - System.currentTimeMillis()) / 1000); + } + + @EventHandler + public void on(final PlayerJoinEvent event) { + final Player player = event.getPlayer(); + + if (database == null) { + return; + } + + database.load( + player, + balance -> { + final Collection commands = + queuedCommands.asMap().remove(player.getUniqueId()); + + if (commands == null) { + return balance; + } + + long total = balance; + + for (final QueuedCommand command : commands) { + final ModifyType type = command.type; + final long amount = command.amount; + total = type.apply(total, amount); + + if (!command.silent) { + plugin + .getLang() + .sendMessage( + player, + true, + "COMMAND." + (type == ModifyType.ADD ? "add" : "remove"), + "amount", + amount); + } + } + + return total; + }); + } + + @EventHandler + public void on(final PlayerQuitEvent event) { + if (database == null) { + return; + } + + final Player player = event.getPlayer(); + queuedCommands.asMap().remove(player.getUniqueId()); + database.save(player); + } + + private class QueuedCommand { + + private final ModifyType type; + private final long amount; + private final boolean silent; + + QueuedCommand(final ModifyType type, final long amount, final boolean silent) { + this.type = type; + this.amount = amount; + this.silent = silent; + } + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/AbstractDatabase.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/AbstractDatabase.java new file mode 100644 index 00000000..54628525 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/AbstractDatabase.java @@ -0,0 +1,61 @@ +package me.realized.tokenmanager.data.database; + +import java.util.List; +import java.util.OptionalLong; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.util.profile.ProfileUtil; +import org.bukkit.entity.Player; + +public abstract class AbstractDatabase implements Database { + + protected final TokenManagerPlugin plugin; + final boolean online; + + AbstractDatabase(final TokenManagerPlugin plugin) { + this.plugin = plugin; + + final String mode = plugin.getConfiguration().getOnlineMode().toLowerCase(); + this.online = mode.equals("auto") ? ProfileUtil.isOnlineMode() : mode.equals("true"); + } + + @Override + public boolean isOnlineMode() { + return online; + } + + OptionalLong from(final Long value) { + return value != null ? OptionalLong.of(value) : OptionalLong.empty(); + } + + String from(final Player player) { + return online ? player.getUniqueId().toString() : player.getName(); + } + + void replaceNames(final List list, final Consumer> callback) { + if (online) { + ProfileUtil.getNames( + list.stream() + .map(element -> UUID.fromString(element.getKey())) + .collect(Collectors.toList()), + result -> { + for (final TopElement element : list) { + final String name = result.get(UUID.fromString(element.getKey())); + + if (name == null) { + element.setKey("&cFailed to get name!"); + continue; + } + + element.setKey(name); + } + + callback.accept(list); + }); + } else { + callback.accept(list); + } + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/Database.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/Database.java new file mode 100644 index 00000000..dfb9bc21 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/Database.java @@ -0,0 +1,89 @@ +package me.realized.tokenmanager.data.database; + +import java.util.List; +import java.util.OptionalLong; +import java.util.function.Consumer; +import java.util.function.Function; +import me.realized.tokenmanager.command.commands.subcommands.OfflineCommand.ModifyType; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public interface Database { + + boolean isOnlineMode(); + + /** + * Checks and creates the table for the plugin if it does not exist. + * + * @throws Exception If the found table does not have the key column matching with the server mode + * (UUID for online/name for offline) + */ + void setup() throws Exception; + + /** + * Gets the cached balance of the player. + * + * @param player Player to get the data + * @return instance of {@link OptionalLong} with the player's token balance if found, otherwise + * empty + */ + OptionalLong get(final Player player); + + void get( + final String key, + final Consumer onLoad, + final Consumer onError, + final boolean create); + + void set(final Player player, final long value); + + void set( + final String key, + final ModifyType type, + final long amount, + final long balance, + final boolean silent, + final Runnable onDone, + final Consumer onError); + + void load(final Player player, final Function modifyLoad); + + void load(final Player player); + + void save(final Player player); + + void shutdown() throws Exception; + + /** + * Returns top balances. Must be called synchronously! + * + * @param limit amount of the rows to be returned + * @param onLoad Consumer to call once data is retrieved + */ + void ordered(final int limit, final Consumer> onLoad); + + void transfer(final CommandSender sender, final Consumer onError); + + class TopElement { + + private final long tokens; + private String key; + + TopElement(final String key, final long tokens) { + this.key = key; + this.tokens = tokens; + } + + public String getKey() { + return key; + } + + void setKey(final String key) { + this.key = key; + } + + public long getTokens() { + return tokens; + } + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/FileDatabase.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/FileDatabase.java new file mode 100644 index 00000000..75e966b5 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/FileDatabase.java @@ -0,0 +1,279 @@ +package me.realized.tokenmanager.data.database; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.OptionalLong; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.function.Function; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.commands.subcommands.OfflineCommand.ModifyType; +import me.realized.tokenmanager.config.Config; +import me.realized.tokenmanager.util.Log; +import me.realized.tokenmanager.util.compat.CompatUtil; +import me.realized.tokenmanager.util.profile.ProfileUtil; +import org.apache.commons.lang3.StringUtils; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; + +public class FileDatabase extends AbstractDatabase { + + private static final String SERVER_MODE_MISMATCH = + "Server is in %s mode, but data.yml does not contain %s! Please delete or rename data.yml to generate a new data.yml with valid format."; + + private final File file; + private final Map data = new HashMap<>(); + + private FileConfiguration config; + + public FileDatabase(final TokenManagerPlugin plugin) throws IOException { + super(plugin); + this.file = new File(plugin.getDataFolder(), "data.yml"); + + if (!file.exists()) { + file.createNewFile(); + } + } + + @Override + public void setup() throws Exception { + this.config = YamlConfiguration.loadConfiguration(file); + final ConfigurationSection section = config.getConfigurationSection("Players"); + + if (section != null) { + for (final String key : section.getKeys(false)) { + if (ProfileUtil.isUUID(key) != online) { + // clear to prevent saving previously loaded data overwriting the file + data.clear(); + throw new Exception( + String.format( + SERVER_MODE_MISMATCH, + online ? "ONLINE" : "OFFLINE", + online ? "UUIDs" : "usernames")); + } + + data.put(key, section.getLong(key)); + } + } + } + + @Override + public OptionalLong get(final Player player) { + return from(data.get(from(player))); + } + + @Override + public void get( + final String key, + final Consumer onLoad, + final Consumer onError, + final boolean create) { + final OptionalLong cached = from(data.get(key)); + + if (!cached.isPresent() && create) { + final long defaultBalance = plugin.getConfiguration().getDefaultBalance(); + data.put(key, defaultBalance); + + if (onLoad != null) { + onLoad.accept(from(defaultBalance)); + } + return; + } + + if (onLoad != null) { + onLoad.accept(cached); + } + } + + @Override + public void set(final Player player, final long value) { + data.put(from(player), value); + } + + @Override + public void set( + final String key, + final ModifyType type, + final long amount, + final long balance, + final boolean silent, + final Runnable onDone, + final Consumer onError) { + plugin.doSync( + () -> { + if (type == ModifyType.SET) { + data.put(key, amount); + return; + } + + final OptionalLong cached = from(data.get(key)); + + if (!cached.isPresent()) { + return; + } + + data.put(key, type.apply(cached.getAsLong(), amount)); + + final Player player; + + if (ProfileUtil.isUUID(key)) { + player = Bukkit.getPlayer(UUID.fromString(key)); + } else { + player = Bukkit.getPlayerExact(key); + } + + if (player == null || silent) { + return; + } + + plugin + .getLang() + .sendMessage( + player, + true, + "COMMAND." + (type == ModifyType.ADD ? "add" : "remove"), + "amount", + amount); + }); + + if (onDone != null) { + onDone.run(); + } + } + + @Override + public void load(final Player player, final Function modifyLoad) { + plugin.doSync( + () -> get(online ? player.getUniqueId().toString() : player.getName(), null, null, true)); + } + + @Override + public void load(final Player player) {} + + @Override + public void save(final Player player) {} + + @Override + public void shutdown() throws IOException { + if (data.isEmpty()) { + return; + } + + Log.info("Saving data to file..."); + data.forEach((key, value) -> config.set("Players." + key, value)); + config.save(file); + Log.info("Save complete."); + } + + @Override + public void ordered(final int limit, final Consumer> consumer) { + final List elements = new ArrayList<>(); + + if (limit <= 0 || data.isEmpty()) { + consumer.accept(elements); + return; + } + + data.forEach( + (key, value) -> { + elements.add(new TopElement(key, value)); + config.set("Players." + key, value); + }); + plugin.doAsync( + () -> { + elements.sort(Comparator.comparingLong(TopElement::getTokens).reversed()); + final List result = + elements.size() > limit ? elements.subList(0, 10) : elements; + replaceNames(result, consumer); + + try { + config.save(file); + } catch (IOException ex) { + Log.error("Failed to save data: " + ex.getMessage()); + ex.printStackTrace(); + } + }); + } + + @Override + public void transfer(final CommandSender sender, final Consumer errorHandler) { + final Config config = plugin.getConfiguration(); + final String query = + String.format( + "SELECT %s, tokens FROM %s;", + online ? "uuid" : "name", + StringUtils.replace(plugin.getConfiguration().getMysqlTable(), "'", "''")); + final HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl( + "jdbc:mysql://" + + config.getMysqlHostname() + + ":" + + config.getMysqlPort() + + "/" + + config.getMysqlDatabase()); + hikariConfig.setDriverClassName( + "com.mysql." + (CompatUtil.isPre1_17() ? "jdbc" : "cj") + ".Driver"); + hikariConfig.setUsername(config.getMysqlUsername()); + hikariConfig.setPassword(config.getMysqlPassword()); + hikariConfig.setMaximumPoolSize(1); + + plugin.doAsync( + () -> { + sender.sendMessage( + ChatColor.BLUE + + plugin.getDescription().getFullName() + + ": Loading user data from MySQL database..."); + + try (HikariDataSource dataSource = new HikariDataSource(hikariConfig); + Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(query); + ResultSet resultSet = statement.executeQuery()) { + sender.sendMessage( + ChatColor.BLUE + + plugin.getDescription().getFullName() + + ": Load Complete. Starting the transfer..."); + + final File file = + new File(plugin.getDataFolder(), "sqldump-" + System.currentTimeMillis() + ".yml"); + file.createNewFile(); + + final FileConfiguration configuration = YamlConfiguration.loadConfiguration(file); + int count = 0; + + while (resultSet.next()) { + final String key = online ? resultSet.getString("uuid") : resultSet.getString("name"); + configuration.set("Players." + key, resultSet.getLong("tokens")); + count++; + } + + configuration.save(file); + sender.sendMessage( + ChatColor.BLUE + + plugin.getDescription().getFullName() + + ": SQL Data saved to " + + file.getName() + + ". Total Transferred Rows: " + + count); + } catch (Exception ex) { + errorHandler.accept(ex.getMessage()); + Log.error("Failed to transfer data from MySQL database: " + ex.getMessage()); + ex.printStackTrace(); + } + }); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/MySQLDatabase.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/MySQLDatabase.java new file mode 100644 index 00000000..d9289a42 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/data/database/MySQLDatabase.java @@ -0,0 +1,539 @@ +package me.realized.tokenmanager.data.database; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import java.io.File; +import java.sql.*; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Function; +import lombok.Getter; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.command.commands.subcommands.OfflineCommand.ModifyType; +import me.realized.tokenmanager.config.Config; +import me.realized.tokenmanager.util.Log; +import me.realized.tokenmanager.util.NumberUtil; +import me.realized.tokenmanager.util.compat.CompatUtil; +import me.realized.tokenmanager.util.profile.ProfileUtil; +import org.apache.commons.lang3.StringUtils; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisPubSub; +import redis.clients.jedis.exceptions.JedisConnectionException; + +public class MySQLDatabase extends AbstractDatabase { + + private static final String SERVER_MODE_MISMATCH = + "Server is in %s mode, but found table '%s' does not have column '%s'! Please choose a different table name."; + private final String table; + private final ExecutorService executor; + private final Map data = new HashMap<>(); + + private HikariDataSource dataSource; + + @Getter private JedisPool jedisPool; + private JedisListener listener; + private transient boolean usingRedis; + + public MySQLDatabase(final TokenManagerPlugin plugin) { + super(plugin); + this.table = StringUtils.replace(plugin.getConfiguration().getMysqlTable(), "'", "''"); + this.executor = Executors.newCachedThreadPool(); + Query.update(table, online); + } + + @Override + public void setup() throws Exception { + final Config config = plugin.getConfiguration(); + final HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl( + config + .getMysqlUrl() + .replace("%hostname%", config.getMysqlHostname()) + .replace("%port%", config.getMysqlPort()) + .replace("%database%", config.getMysqlDatabase())); + hikariConfig.setDriverClassName( + "com.mysql." + (CompatUtil.isPre1_17() ? "" : "cj.") + "jdbc.Driver"); + hikariConfig.setUsername(config.getMysqlUsername()); + hikariConfig.setPassword(config.getMysqlPassword()); + + this.dataSource = new HikariDataSource(hikariConfig); + + if (config.isRedisEnabled()) { + final String password = config.getRedisPassword(); + + if (password.isEmpty()) { + this.jedisPool = + new JedisPool(new JedisPoolConfig(), config.getRedisServer(), config.getRedisPort(), 0); + } else { + this.jedisPool = + new JedisPool( + new JedisPoolConfig(), config.getRedisServer(), config.getRedisPort(), 0, password); + } + + plugin.doAsync( + () -> { + usingRedis = true; + + try (Jedis jedis = jedisPool.getResource()) { + jedis.subscribe(listener = new JedisListener(), "tokenmanager"); + } catch (Exception ex) { + usingRedis = false; + Log.error( + "Failed to connect to the redis server! Player balance synchronization issues may occur when modifying them while offline."); + Log.error("Cause of error: " + ex.getMessage()); + } + }); + } + + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + statement.execute(Query.CREATE_TABLE.query); + + try (ResultSet resultSet = connection.getMetaData().getColumns(null, null, table, "name")) { + if (resultSet.isBeforeFirst() == online) { + throw new Exception( + String.format( + SERVER_MODE_MISMATCH, + online ? "ONLINE" : "OFFLINE", + table, + online ? "uuid" : "name")); + } + } + } + } + + @Override + public OptionalLong get(final Player player) { + return from(data.get(player.getUniqueId())); + } + + @Override + public void get( + final String key, + final Consumer onLoad, + final Consumer onError, + final boolean create) { + try (Connection connection = dataSource.getConnection()) { + onLoad.accept(select(connection, key, create)); + } catch (Exception ex) { + if (onError != null) { + onError.accept(ex.getMessage()); + } + + Log.error("Failed to obtain data for " + key + ": " + ex.getMessage()); + ex.printStackTrace(); + } + } + + @Override + public void set(final Player player, final long value) { + data.put(player.getUniqueId(), value); + } + + @Override + public void set( + final String key, + final ModifyType type, + final long amount, + final long balance, + final boolean silent, + final Runnable onDone, + final Consumer onError) { + plugin.doAsync( + () -> { + try (Connection connection = dataSource.getConnection()) { + update(connection, key, balance); + + if (usingRedis) { + publish(key + ":" + type.name() + ":" + amount + ":" + silent); + } else { + plugin.doSync(() -> onModification(key, type, amount, silent)); + } + + if (onDone != null) { + onDone.run(); + } + } catch (Exception ex) { + if (onError != null) { + onError.accept(ex.getMessage()); + } + + Log.error("Failed to save data for " + key + ": " + ex.getMessage()); + ex.printStackTrace(); + } + }); + } + + @Override + public void load(final Player player, final Function modifyLoad) { + plugin.doAsync( + () -> + get( + from(player), + balance -> { + if (!balance.isPresent()) { + return; + } + + plugin.doSync( + () -> { + // Cancel caching if player has left before loading was completed + if (!player.isOnline()) { + return; + } + + long totalBalance = balance.getAsLong(); + + if (modifyLoad != null) { + totalBalance = modifyLoad.apply(totalBalance); + } + + data.put(player.getUniqueId(), totalBalance); + }); + }, + null, + true)); + } + + @Override + public void load(final Player player) { + load(player, null); + } + + @Override + public void save(final Player player) { + final OptionalLong balance = from(data.remove(player.getUniqueId())); + + if (!balance.isPresent()) { + return; + } + + executor.execute( + () -> { + try (Connection connection = dataSource.getConnection()) { + update(connection, from(player), balance.getAsLong()); + } catch (Exception ex) { + ex.printStackTrace(); + } + }); + } + + @Override + public void shutdown() throws Exception { + executor.shutdown(); + + if (!executor.awaitTermination(2, TimeUnit.SECONDS)) { + Log.error("Some tasks have failed to execute!"); + } + + try (Connection connection = dataSource.getConnection()) { + insertCache(connection, data, true); + } finally { + for (final AutoCloseable closeable : Arrays.asList(dataSource, listener, jedisPool)) { + if (closeable != null) { + try { + closeable.close(); + } catch (Exception ex) { + Log.error( + "Failed to close " + closeable.getClass().getSimpleName() + ": " + ex.getMessage()); + ex.printStackTrace(); + } + } + } + } + } + + @Override + public void ordered(final int limit, final Consumer> onLoad) { + final List result = new ArrayList<>(); + + if (limit <= 0) { + onLoad.accept(result); + return; + } + + // Create a copy of the current cache to prevent HashMap being accessed by multiple threads + final Map copy = new HashMap<>(data); + + plugin.doAsync( + () -> { + try (Connection connection = dataSource.getConnection()) { + insertCache(connection, copy, false); + connection.setAutoCommit(true); + + try (PreparedStatement statement = + connection.prepareStatement(Query.SELECT_WITH_LIMIT.query)) { + statement.setInt(1, limit); + + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + final String key = + online ? resultSet.getString("uuid") : resultSet.getString("name"); + result.add(new TopElement(key, (int) resultSet.getLong("tokens"))); + } + + replaceNames(result, onLoad); + } + } + } catch (Exception ex) { + Log.error("Failed to load top balances: " + ex.getMessage()); + ex.printStackTrace(); + } + }); + } + + @Override + public void transfer(final CommandSender sender, final Consumer onError) { + plugin.doAsync( + () -> { + final File file = new File(plugin.getDataFolder(), "data.yml"); + + if (!file.exists()) { + sender.sendMessage(ChatColor.RED + "File not found!"); + return; + } + + sender.sendMessage( + ChatColor.BLUE + + plugin.getDescription().getFullName() + + ": Loading user data from " + + file.getName() + + "..."); + + final FileConfiguration config = YamlConfiguration.loadConfiguration(file); + final ConfigurationSection section = config.getConfigurationSection("Players"); + + if (section == null) { + sender.sendMessage(ChatColor.RED + "Data not found!"); + return; + } + + sender.sendMessage( + ChatColor.BLUE + + plugin.getDescription().getFullName() + + ": Load Complete. Starting the transfer..."); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = + connection.prepareStatement(Query.INSERT_OR_UPDATE.query)) { + connection.setAutoCommit(false); + int i = 0; + final Set keys = section.getKeys(false); + + for (final String key : keys) { + final long value = section.getLong(key); + statement.setString(1, key); + statement.setLong(2, value); + statement.setLong(3, value); + statement.addBatch(); + + if (++i % 100 == 0 || i == keys.size()) { + statement.executeBatch(); + } + } + + connection.commit(); + connection.setAutoCommit(true); + sender.sendMessage( + ChatColor.BLUE + + plugin.getDescription().getFullName() + + ": Transfer Complete. Total Transferred Data: " + + keys.size()); + } catch (SQLException ex) { + onError.accept(ex.getMessage()); + Log.error("Failed to transfer data from file: " + ex.getMessage()); + ex.printStackTrace(); + } + }); + } + + private void insertCache( + final Connection connection, final Map cache, final boolean remove) + throws SQLException { + try (PreparedStatement statement = connection.prepareStatement(Query.UPDATE.query)) { + connection.setAutoCommit(false); + + int i = 0; + final Collection players = Bukkit.getOnlinePlayers(); + + for (final Player player : players) { + final Optional balance = + Optional.ofNullable( + remove ? cache.remove(player.getUniqueId()) : cache.get(player.getUniqueId())); + + if (!balance.isPresent()) { + continue; + } + + statement.setLong(1, balance.get()); + statement.setString(2, online ? player.getUniqueId().toString() : player.getName()); + statement.addBatch(); + + if (++i % 100 == 0 || i == players.size()) { + statement.executeBatch(); + } + } + } finally { + connection.commit(); + } + } + + private OptionalLong select(final Connection connection, final String key, final boolean create) + throws Exception { + try (PreparedStatement selectStatement = connection.prepareStatement(Query.SELECT_ONE.query)) { + selectStatement.setString(1, key); + + try (ResultSet resultSet = selectStatement.executeQuery()) { + if (!resultSet.next()) { + if (create) { + final long defaultBalance = plugin.getConfiguration().getDefaultBalance(); + + try (PreparedStatement insertStatement = + connection.prepareStatement(Query.INSERT.query)) { + insertStatement.setString(1, key); + insertStatement.setLong(2, plugin.getConfiguration().getDefaultBalance()); + insertStatement.execute(); + } + + return OptionalLong.of(defaultBalance); + } + + return OptionalLong.empty(); + } + + return OptionalLong.of(resultSet.getLong("tokens")); + } + } + } + + private void update(final Connection connection, final String key, final long value) + throws Exception { + try (PreparedStatement statement = connection.prepareStatement(Query.UPDATE.query)) { + statement.setLong(1, value); + statement.setString(2, key); + statement.execute(); + } + } + + private void onModification( + final String key, final ModifyType type, final long amount, final boolean silent) { + final Player player; + + if (ProfileUtil.isUUID(key)) { + player = Bukkit.getPlayer(UUID.fromString(key)); + } else { + player = Bukkit.getPlayerExact(key); + } + + if (player == null) { + return; + } + + if (type == ModifyType.SET) { + set(player, amount); + return; + } + + final OptionalLong cached; + + if (!(cached = get(player)).isPresent()) { + return; + } + + set(player, type.apply(cached.getAsLong(), amount)); + + if (silent) { + return; + } + + plugin + .getLang() + .sendMessage( + player, + true, + "COMMAND." + (type == ModifyType.ADD ? "add" : "remove"), + "amount", + amount); + } + + private void publish(final String message) { + try (Jedis jedis = jedisPool.getResource()) { + jedis.publish("tokenmanager", message); + } catch (JedisConnectionException ignored) { + } + } + + private enum Query { + CREATE_TABLE( + "CREATE TABLE IF NOT EXISTS {table} (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, {column} NOT NULL UNIQUE, tokens BIGINT(255) NOT NULL);"), + SELECT_WITH_LIMIT("SELECT {identifier}, tokens FROM {table} ORDER BY tokens DESC LIMIT ?;"), + SELECT_ONE("SELECT tokens FROM {table} WHERE {identifier}=?;"), + INSERT("INSERT INTO {table} ({identifier}, tokens) VALUES (?, ?);"), + UPDATE("UPDATE {table} SET tokens=? WHERE {identifier}=?;"), + INSERT_OR_UPDATE( + "INSERT INTO {table} ({identifier}, tokens) VALUES (?, ?) ON DUPLICATE KEY UPDATE tokens=?;"); + + private String query; + + Query(final String query) { + this.query = query; + } + + private static void update(final String table, final boolean online) { + for (final Query query : values()) { + query.replace( + s -> s.replace("{table}", table).replace("{identifier}", online ? "uuid" : "name")); + + if (query == CREATE_TABLE) { + query.replace( + s -> s.replace("{column}", online ? "uuid VARCHAR(36)" : "name VARCHAR(16)")); + } + } + } + + private void replace(final Function function) { + this.query = function.apply(query); + } + } + + private class JedisListener extends JedisPubSub implements AutoCloseable { + + @Override + public void onMessage(final String channel, final String message) { + final String[] args = message.split(":"); + + if (args.length < 3) { + return; + } + + plugin.doSync( + () -> { + final ModifyType type = ModifyType.valueOf(args[1]); + final OptionalLong amount = NumberUtil.parseLong(args[2]); + + if (!amount.isPresent()) { + return; + } + + onModification(args[0], type, amount.getAsLong(), args[3].equals("true")); + }); + } + + @Override + public void close() { + unsubscribe(); + } + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/event/TokenReceiveEvent.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/event/TokenReceiveEvent.java new file mode 100644 index 00000000..476813ef --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/event/TokenReceiveEvent.java @@ -0,0 +1,61 @@ +package me.realized.tokenmanager.event; + +import java.util.UUID; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * @deprecated as of v3.2.0. Use {@link me.realized.tokenmanager.api.event.TMTokenSendEvent} + * instead. + */ +@Deprecated +public class TokenReceiveEvent extends Event implements Cancellable { + + private static final HandlerList handlers = new HandlerList(); + + private final UUID receiver; + private int amount; + private boolean cancelled; + + @Deprecated + public TokenReceiveEvent(final UUID receiver, final int amount) { + this.receiver = receiver; + this.amount = amount; + } + + @Deprecated + public static HandlerList getHandlerList() { + return handlers; + } + + @Deprecated + public UUID getReceiver() { + return receiver; + } + + @Deprecated + public int getAmount() { + return amount; + } + + @Deprecated + public void setAmount(final int amount) { + this.amount = amount; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(final boolean cancelled) { + this.cancelled = cancelled; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/hook/HookManager.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/hook/HookManager.java new file mode 100644 index 00000000..02a7fc10 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/hook/HookManager.java @@ -0,0 +1,22 @@ +package me.realized.tokenmanager.hook; + +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.hook.hooks.PlaceholderHook; +import me.realized.tokenmanager.hook.hooks.VaultHook; +import me.realized.tokenmanager.util.hook.AbstractHookManager; + +public class HookManager extends AbstractHookManager { + + public HookManager(final TokenManagerPlugin plugin) { + super(plugin); + } + + @Override + public void handleLoad() { + register("PlaceholderAPI", PlaceholderHook.class); + register("Vault", VaultHook.class); + } + + @Override + public void handleUnload() {} +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/hook/hooks/PlaceholderHook.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/hook/hooks/PlaceholderHook.java new file mode 100644 index 00000000..fad2dfe9 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/hook/hooks/PlaceholderHook.java @@ -0,0 +1,52 @@ +package me.realized.tokenmanager.hook.hooks; + +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.util.hook.PluginHook; +import org.bukkit.entity.Player; + +public class PlaceholderHook extends PluginHook { + + public PlaceholderHook(final TokenManagerPlugin plugin) { + super(plugin, "PlaceholderAPI"); + new Placeholders().register(); + } + + public class Placeholders extends PlaceholderExpansion { + + @Override + public String getIdentifier() { + return "tm"; + } + + @Override + public String getRequiredPlugin() { + return plugin.getName(); + } + + @Override + public boolean canRegister() { + return true; + } + + @Override + public String getAuthor() { + return plugin.getDescription().getAuthors().get(0); + } + + @Override + public String getVersion() { + return plugin.getDescription().getVersion(); + } + + @Override + public boolean persist() { + return true; + } + + @Override + public String onPlaceholderRequest(final Player player, final String identifier) { + return plugin.handlePlaceholderRequest(player, identifier); + } + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/hook/hooks/VaultHook.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/hook/hooks/VaultHook.java new file mode 100644 index 00000000..f14e64fe --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/hook/hooks/VaultHook.java @@ -0,0 +1,272 @@ +package me.realized.tokenmanager.hook.hooks; + +import java.util.List; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.util.hook.PluginHook; +import net.milkbowl.vault.economy.Economy; +import net.milkbowl.vault.economy.EconomyResponse; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.plugin.ServicePriority; + +public class VaultHook extends PluginHook { + + public VaultHook(final TokenManagerPlugin plugin) { + super(plugin, "Vault"); + if (plugin.getConfiguration().isRegisterEconomy()) { + Bukkit.getServicesManager() + .register(Economy.class, new TokenManagerEconomy(), plugin, ServicePriority.Highest); + } + } + + private class TokenManagerEconomy implements Economy { + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public String getName() { + return "TokenManager"; + } + + @Override + public int fractionalDigits() { + return -1; + } + + @Override + public String format(double v) { + return v + " " + (v > 1 ? currencyNamePlural() : currencyNameSingular()); + } + + @Override + public String currencyNamePlural() { + return "tokens"; + } + + @Override + public String currencyNameSingular() { + return "token"; + } + + @Override + public double getBalance(String name) { + return getBalance(Bukkit.getPlayerExact(name)); + } + + @Override + public double getBalance(OfflinePlayer player) { + return (player != null && player.getPlayer() != null) + ? plugin.getTokens(player.getPlayer()).orElse(0) + : 0; + } + + @Override + public double getBalance(String name, String world) { + return getBalance(name); + } + + @Override + public double getBalance(OfflinePlayer player, String world) { + return getBalance(player); + } + + @Override + public boolean has(String name, double amount) { + return getBalance(name) >= (long) amount; + } + + @Override + public boolean has(OfflinePlayer player, double amount) { + return getBalance(player) >= amount; + } + + @Override + public boolean has(String name, String world, double amount) { + return has(name, amount); + } + + @Override + public boolean has(OfflinePlayer player, String world, double amount) { + return has(player, amount); + } + + @Override + public EconomyResponse withdrawPlayer(String name, double amount) { + return withdrawPlayer(Bukkit.getPlayerExact(name), amount); + } + + @Override + public EconomyResponse withdrawPlayer(OfflinePlayer player, double amount) { + if (player == null) { + return new EconomyResponse( + 0, 0, EconomyResponse.ResponseType.FAILURE, "Player is not online"); + } + + long balance = (long) getBalance(player); + + if (amount == 0) { + return new EconomyResponse(amount, balance, EconomyResponse.ResponseType.SUCCESS, ""); + } + + if (balance < amount) { + return new EconomyResponse( + amount, balance, EconomyResponse.ResponseType.FAILURE, "Not enough tokens"); + } + + balance = balance - (long) amount; + plugin.setTokens(player.getPlayer(), balance); + return new EconomyResponse(amount, balance, EconomyResponse.ResponseType.SUCCESS, ""); + } + + @Override + public EconomyResponse withdrawPlayer(String name, String world, double amount) { + return withdrawPlayer(name, amount); + } + + @Override + public EconomyResponse withdrawPlayer(OfflinePlayer player, String name, double amount) { + return withdrawPlayer(player, amount); + } + + @Override + public EconomyResponse depositPlayer(String name, double amount) { + return depositPlayer(Bukkit.getPlayer(name), amount); + } + + @Override + public EconomyResponse depositPlayer(OfflinePlayer player, double amount) { + if (player == null) { + return new EconomyResponse( + 0, 0, EconomyResponse.ResponseType.FAILURE, "Player is not online"); + } + + long balance = (long) getBalance(player); + + if (amount == 0) { + return new EconomyResponse(amount, balance, EconomyResponse.ResponseType.SUCCESS, ""); + } + + balance = balance + (long) amount; + plugin.setTokens(player.getPlayer(), balance); + return new EconomyResponse(amount, balance, EconomyResponse.ResponseType.SUCCESS, ""); + } + + @Override + public EconomyResponse depositPlayer(String name, String world, double amount) { + return depositPlayer(name, amount); + } + + @Override + public EconomyResponse depositPlayer(OfflinePlayer player, String world, double amount) { + return depositPlayer(player, amount); + } + + @Override + public boolean hasBankSupport() { + return false; + } + + @Override + public boolean createPlayerAccount(String name) { + return true; + } + + @Override + public boolean createPlayerAccount(OfflinePlayer player) { + return true; + } + + @Override + public boolean createPlayerAccount(String name, String world) { + return true; + } + + @Override + public boolean createPlayerAccount(OfflinePlayer player, String world) { + return true; + } + + @Override + public boolean hasAccount(String name) { + return true; + } + + @Override + public boolean hasAccount(OfflinePlayer player) { + return true; + } + + @Override + public boolean hasAccount(String name, String world) { + return true; + } + + @Override + public boolean hasAccount(OfflinePlayer player, String world) { + return true; + } + + @Override + public EconomyResponse createBank(String s, String s1) { + return null; + } + + @Override + public EconomyResponse createBank(String s, OfflinePlayer offlinePlayer) { + return null; + } + + @Override + public EconomyResponse deleteBank(String s) { + return null; + } + + @Override + public EconomyResponse bankBalance(String s) { + return null; + } + + @Override + public EconomyResponse bankHas(String s, double v) { + return null; + } + + @Override + public EconomyResponse bankWithdraw(String s, double v) { + return null; + } + + @Override + public EconomyResponse bankDeposit(String s, double v) { + return null; + } + + @Override + public EconomyResponse isBankOwner(String s, String s1) { + return null; + } + + @Override + public EconomyResponse isBankOwner(String s, OfflinePlayer offlinePlayer) { + return null; + } + + @Override + public EconomyResponse isBankMember(String s, String s1) { + return null; + } + + @Override + public EconomyResponse isBankMember(String s, OfflinePlayer offlinePlayer) { + return null; + } + + @Override + public List getBanks() { + return null; + } + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/Shop.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/Shop.java new file mode 100644 index 00000000..eae709b4 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/Shop.java @@ -0,0 +1,83 @@ +package me.realized.tokenmanager.shop; + +import java.util.HashMap; +import java.util.Map; +import lombok.AccessLevel; +import lombok.Getter; +import me.realized.tokenmanager.util.Placeholders; +import me.realized.tokenmanager.util.StringUtil; +import org.bukkit.Bukkit; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +public class Shop { + + @Getter private final String name; + private final String title; + + @Getter(value = AccessLevel.PACKAGE) + private final Inventory inventory; + + @Getter private final boolean autoClose; + @Getter private final boolean usePermission; + @Getter private final boolean confirmPurchase; + + private Map slots; + + public Shop( + final String name, + final String title, + final int rows, + final boolean autoClose, + final boolean usePermission, + final boolean confirmPurchase) + throws IllegalArgumentException { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("Name is null or empty."); + } + + if (name.contains("-")) { + throw new IllegalArgumentException( + "Shop name cannot contain a dash. (This is implemented to prevent errors with shop slot permissions)"); + } + + this.name = name; + + if (title.length() > 32) { + throw new IllegalArgumentException("Shop title cannot be longer than 32 characters."); + } + + if (rows <= 0 || rows > 6) { + throw new IllegalArgumentException("Shop rows must be in between 1 - 6."); + } + + this.title = StringUtil.color(title); + this.inventory = Bukkit.createInventory(null, rows * 9, title); + this.autoClose = autoClose; + this.usePermission = usePermission; + this.confirmPurchase = confirmPurchase; + } + + public String getTitle() { + return title; + } + + public int getSize() { + return inventory.getSize(); + } + + public void setSlot(final int slot, final ItemStack displayed, final Slot data) { + Placeholders.replace(displayed, data.getCost(), "price"); + inventory.setItem(slot, displayed); + + if (slots == null) { + slots = new HashMap<>(); + } + + slots.put(slot, data); + } + + public Slot getSlot(final int slot) { + return slots != null ? slots.get(slot) : null; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/ShopConfig.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/ShopConfig.java new file mode 100644 index 00000000..6c7ebe08 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/ShopConfig.java @@ -0,0 +1,202 @@ +package me.realized.tokenmanager.shop; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalLong; +import lombok.Getter; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.config.Config; +import me.realized.tokenmanager.shop.gui.guis.ConfirmGui; +import me.realized.tokenmanager.util.Log; +import me.realized.tokenmanager.util.NumberUtil; +import me.realized.tokenmanager.util.Reloadable; +import me.realized.tokenmanager.util.StringUtil; +import me.realized.tokenmanager.util.compat.Items; +import me.realized.tokenmanager.util.config.AbstractConfiguration; +import me.realized.tokenmanager.util.inventory.GUIBuilder; +import me.realized.tokenmanager.util.inventory.GUIBuilder.Pattern; +import me.realized.tokenmanager.util.inventory.ItemBuilder; +import me.realized.tokenmanager.util.inventory.ItemUtil; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +public class ShopConfig extends AbstractConfiguration implements Reloadable { + + private final Map shopSamples = new LinkedHashMap<>(); + + @Getter private Inventory confirmGuiSample; + + public ShopConfig(final TokenManagerPlugin plugin) { + super(plugin, "shops"); + } + + @Override + protected void loadValues(final FileConfiguration configuration) { + final ConfigurationSection section = configuration.getConfigurationSection("shops"); + + if (section == null) { + return; + } + + for (String name : section.getKeys(false)) { + final ConfigurationSection shopSection = section.getConfigurationSection(name); + final Shop shop; + name = name.toLowerCase(); + + try { + shop = + new Shop( + name, + shopSection.getString("title", "&cShop title was not specified."), + shopSection.getInt("rows", 1), + shopSection.getBoolean("auto-close", false), + shopSection.getBoolean("use-permission", false), + shopSection.getBoolean("confirm-purchase", false)); + } catch (IllegalArgumentException ex) { + Log.error(this, "Failed to initialize shop '" + name + "': " + ex.getMessage()); + continue; + } + + final ConfigurationSection itemsSection = shopSection.getConfigurationSection("items"); + + if (itemsSection != null) { + for (final String num : itemsSection.getKeys(false)) { + final OptionalLong target = NumberUtil.parseLong(num); + + if (!target.isPresent()) { + Log.error( + this, + "Failed to load slot '" + + num + + "' of shop '" + + name + + "': '" + + num + + "' is not a valid number."); + continue; + } + + final long slot = target.getAsLong(); + + if (slot < 0 || slot >= shop.getInventory().getSize()) { + Log.error( + this, + "Failed to load slot '" + + num + + "' of shop '" + + name + + "': '" + + slot + + "' is over the shop size."); + continue; + } + + final ConfigurationSection slotSection = itemsSection.getConfigurationSection(num); + final ItemStack displayed; + + try { + displayed = ItemUtil.loadFromString(slotSection.getString("displayed")); + } catch (Exception ex) { + shop.getInventory() + .setItem( + (int) slot, + ItemBuilder.of(Material.REDSTONE_BLOCK) + .name("&4&m------------------") + .lore( + "&cThere was an error", + "&cwhile loading this", + "&citem, please contact", + "&can administrator.", + "&4&m------------------") + .build()); + Log.error( + this, + "Failed to load displayed item for slot '" + + num + + "' of shop '" + + name + + "': " + + ex.getMessage()); + continue; + } + + final Object value = slotSection.get("message"); + + shop.setSlot( + (int) slot, + displayed, + new Slot( + plugin, + shop, + (int) slot, + slotSection.getInt("cost", 1000000), + slotSection.getInt("empty-slots-required", 0), + displayed, + value != null + ? (value instanceof List + ? StringUtil.fromList((List) value) + : value.toString()) + : null, + slotSection.getString("subshop"), + slotSection.getStringList("commands"), + slotSection.getBoolean("use-permission", false), + slotSection.getBoolean("confirm-purchase", false))); + } + } + + if (!register(name, shop)) { + Log.error(this, "Failed to load shop '" + name + "': Shop already exists."); + } + } + + final Config config = plugin.getConfiguration(); + + this.confirmGuiSample = + GUIBuilder.of(StringUtil.color(config.getConfirmPurchaseTitle()), 3) + .pattern( + Pattern.of("AAABBBCCC", "AAABBBCCC", "AAABBBCCC") + .specify('A', Items.GREEN_PANE.clone()) + .specify('B', Items.GRAY_PANE.clone()) + .specify('C', Items.RED_PANE.clone())) + .set( + ConfirmGui.CONFIRM_PURCHASE_SLOT, + ItemUtil.loadFromString( + config.getConfirmPurchaseConfirm(), + error -> Log.error(this, "Failed to load confirm-button: " + error))) + .set( + ConfirmGui.CANCEL_PURCHASE_SLOT, + ItemUtil.loadFromString( + config.getConfirmPurchaseCancel(), + error -> Log.error(this, "Failed to load cancel-button: " + error))) + .build(); + } + + @Override + public void handleUnload() { + shopSamples.clear(); + plugin.getShopManager().clearCache(); + } + + public Optional getShop(final String name) { + return Optional.ofNullable(shopSamples.get(name)); + } + + public Collection getShops() { + return shopSamples.values(); + } + + public boolean register(final String name, final Shop shop) { + if (shopSamples.containsKey(name)) { + return false; + } + + shopSamples.put(name, shop); + return true; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/ShopManager.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/ShopManager.java new file mode 100644 index 00000000..47320a76 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/ShopManager.java @@ -0,0 +1,137 @@ +package me.realized.tokenmanager.shop; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.config.Config; +import me.realized.tokenmanager.config.Lang; +import me.realized.tokenmanager.shop.gui.BaseGui; +import me.realized.tokenmanager.util.Loadable; +import me.realized.tokenmanager.util.StringUtil; +import me.realized.tokenmanager.util.inventory.InventoryUtil; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.Inventory; + +public class ShopManager implements Loadable, Listener { + + private final TokenManagerPlugin plugin; + private final Config config; + private final Lang lang; + + private final Map cache = new HashMap<>(); + private final Map cooldowns = new HashMap<>(); + + public ShopManager(final TokenManagerPlugin plugin) { + this.plugin = plugin; + this.config = plugin.getConfiguration(); + this.lang = plugin.getLang(); + plugin.getServer().getPluginManager().registerEvents(this, plugin); + } + + @Override + public void handleLoad() {} + + @Override + public void handleUnload() { + clearCache(); + } + + void clearCache() { + cooldowns.clear(); + + if (cache.isEmpty()) { + return; + } + + Bukkit.getOnlinePlayers() + .forEach( + player -> { + final Inventory top = player.getOpenInventory().getTopInventory(); + final BaseGui gui = cache.get(player.getUniqueId()); + + if (gui == null || !gui.isGui(top)) { + return; + } + + player.closeInventory(); + player.sendMessage( + StringUtil.color( + "&cShop was automatically closed since the plugin is deactivating.")); + }); + cache.clear(); + } + + public void open(final Player player, final BaseGui gui) { + cache.put(player.getUniqueId(), gui); + gui.refresh(player, true); + gui.open(player); + } + + public Optional find(final Inventory inventory) { + return cache.values().stream() + .filter(gui -> gui.isGui(inventory)) + .findFirst() + .map(BaseGui::getShop); + } + + @EventHandler + public void on(final InventoryClickEvent event) { + final Player player = (Player) event.getWhoClicked(); + + if (event.getRawSlot() < 0) { + return; + } + + final Inventory clicked = + InventoryUtil.getClickedInventory(event.getRawSlot(), event.getView()); + final Inventory top = player.getOpenInventory().getTopInventory(); + + if (clicked == null) { + return; + } + + final BaseGui gui = cache.get(player.getUniqueId()); + + if (gui == null || !gui.isGui(top)) { + return; + } + + event.setCancelled(true); + + if (!clicked.equals(top)) { + return; + } + + final long now = System.currentTimeMillis(); + final long remaining = + cooldowns.getOrDefault(player.getUniqueId(), 0L) + config.getClickDelay() * 1000L - now; + + if (remaining > 0) { + plugin.doSync(player::closeInventory); + lang.sendMessage( + player, + true, + "ERROR.on-click-cooldown", + "remaining", + StringUtil.format(remaining / 1000 + (remaining % 1000 > 0 ? 1 : 0))); + return; + } + + if (gui.handle(player, event.getSlot())) { + cooldowns.put(player.getUniqueId(), now); + } + } + + @EventHandler + public void on(final PlayerQuitEvent event) { + cooldowns.remove(event.getPlayer().getUniqueId()); + cache.remove(event.getPlayer().getUniqueId()); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/Slot.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/Slot.java new file mode 100644 index 00000000..14dda671 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/Slot.java @@ -0,0 +1,182 @@ +package me.realized.tokenmanager.shop; + +import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; +import lombok.Getter; +import me.realized.tokenmanager.Permissions; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.api.event.TMShopPurchaseEvent; +import me.realized.tokenmanager.config.Config; +import me.realized.tokenmanager.shop.gui.guis.ConfirmGui; +import me.realized.tokenmanager.shop.gui.guis.ShopGui; +import me.realized.tokenmanager.util.Placeholders; +import me.realized.tokenmanager.util.inventory.InventoryUtil; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class Slot { + + private static final String PURCHASE_LOG = "%s (%s) purchased %s:%s for %s tokens! (%s -> %s)"; + + @Getter private final TokenManagerPlugin plugin; + @Getter private final Config config; + @Getter private final Shop shop; + @Getter private final int slot; + @Getter private final int cost; + @Getter private final int emptySlotsRequired; + @Getter private final ItemStack displayed; + @Getter private final String message; + @Getter private final String subshop; + @Getter private final List commands; + @Getter private final boolean usePermission; + @Getter private final boolean confirmPurchase; + + public Slot( + final TokenManagerPlugin plugin, + final Shop shop, + final int slot, + final int cost, + final int emptySlotsRequired, + final ItemStack displayed, + final String message, + final String subshop, + final List commands, + final boolean usePermission, + final boolean confirmPurchase) { + this.plugin = plugin; + this.config = plugin.getConfiguration(); + this.shop = shop; + this.slot = slot; + this.cost = cost; + this.emptySlotsRequired = emptySlotsRequired; + this.displayed = displayed; + this.message = message != null ? Placeholders.replace(message, cost, "price", "cost") : null; + this.subshop = subshop; + this.commands = commands; + this.usePermission = usePermission; + this.confirmPurchase = confirmPurchase; + commands.replaceAll( + command -> { + command = Placeholders.replace(command, cost, "price", "cost"); + + if (command.startsWith("/")) { + command = command.substring(1); + } + + return command; + }); + } + + public boolean purchase(final Player player, final boolean confirmPurchase, final boolean close) { + if (plugin.getConfiguration().isCheckInventoryFull() && InventoryUtil.isInventoryFull(player)) { + plugin.getLang().sendMessage(player, true, "ERROR.inventory-is-full"); + return false; + } + + final OptionalLong cached = plugin.getDataManager().get(player); + + if (!cached.isPresent()) { + plugin.doSync(player::closeInventory); + plugin + .getLang() + .sendMessage(player, false, "&cYour data is improperly loaded, please re-log."); + return false; + } + + long balance = cached.getAsLong(); + + if (balance - cost < 0) { + plugin.doSync(player::closeInventory); + plugin + .getLang() + .sendMessage(player, true, "ERROR.balance-not-enough", "needed", cost - balance); + return false; + } + + final TMShopPurchaseEvent event = new TMShopPurchaseEvent(player, cost, shop, this); + plugin.getServer().getPluginManager().callEvent(event); + + if (event.isCancelled()) { + return false; + } + + if (cost > 0) { + // If confirm-purchase is true for shop or slot + if (confirmPurchase) { + plugin.getShopManager().open(player, new ConfirmGui(plugin, shop, slot)); + return false; + } + + plugin.getDataManager().set(player, balance = balance - cost); + } + + if (config.isLogPurchases()) { + plugin + .getLogger() + .info( + String.format( + PURCHASE_LOG, + player.getUniqueId(), + player.getName(), + shop.getName(), + slot, + cost, + balance + cost, + balance)); + } + + if (commands != null) { + for (final String command : commands) { + Bukkit.dispatchCommand( + Bukkit.getConsoleSender(), + Placeholders.replace(command, balance, "balance", "tokens") + .replace("%player%", player.getName())); + } + } + + if (message != null && !message.isEmpty()) { + plugin + .getLang() + .sendMessage( + player, + false, + Placeholders.replace(message, balance, "balance", "tokens"), + "player", + player.getName()); + } + + if (subshop != null && !subshop.isEmpty()) { + final Optional result = plugin.getShopConfig().getShop(subshop); + + if (!result.isPresent()) { + plugin.getLang().sendMessage(player, true, "ERROR.shop-not-found", "input", subshop); + return true; + } + + final Shop target = result.get(); + + if (target.isUsePermission() && !player.hasPermission(Permissions.SHOP + target.getName())) { + plugin + .getLang() + .sendMessage( + player, + true, + "ERROR.no-permission", + "permission", + Permissions.SHOP + target.getName()); + return true; + } + + plugin.getShopManager().open(player, new ShopGui(plugin, target)); + return true; + } + + if (shop.isAutoClose() || close) { + plugin.doSync(player::closeInventory); + } + + return true; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/gui/BaseGui.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/gui/BaseGui.java new file mode 100644 index 00000000..92afda6c --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/gui/BaseGui.java @@ -0,0 +1,48 @@ +package me.realized.tokenmanager.shop.gui; + +import lombok.Getter; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.shop.Shop; +import me.realized.tokenmanager.shop.ShopManager; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; + +public abstract class BaseGui { + + protected final TokenManagerPlugin plugin; + + @Getter protected final Shop shop; + protected final Inventory inventory; + + protected BaseGui(final TokenManagerPlugin plugin, final Shop shop, final Inventory inventory) { + this.plugin = plugin; + this.shop = shop; + this.inventory = inventory; + } + + public boolean isGui(final Inventory inventory) { + return this.inventory.equals(inventory); + } + + public void open(final Player player) { + player.openInventory(inventory); + } + + /** + * Updates gui items with info from the player. + * + * @param player Player to provide the info. + * @param firstLoad Whether or not this refresh is caused by {@link ShopManager#open(Player, + * BaseGui)}. + */ + public abstract void refresh(final Player player, final boolean firstLoad); + + /** + * Handles inventory click for gui. + * + * @param player Player that clicked in the gui. + * @param slot Slot that was clicked. + * @return true if a purchase was made. false otherwise + */ + public abstract boolean handle(final Player player, final int slot); +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/gui/guis/ConfirmGui.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/gui/guis/ConfirmGui.java new file mode 100644 index 00000000..6910df5f --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/gui/guis/ConfirmGui.java @@ -0,0 +1,72 @@ +package me.realized.tokenmanager.shop.gui.guis; + +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.data.DataManager; +import me.realized.tokenmanager.shop.Shop; +import me.realized.tokenmanager.shop.ShopManager; +import me.realized.tokenmanager.shop.gui.BaseGui; +import me.realized.tokenmanager.util.Placeholders; +import me.realized.tokenmanager.util.inventory.InventoryUtil; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class ConfirmGui extends BaseGui { + + public static final int CONFIRM_PURCHASE_SLOT = 10; + public static final int CANCEL_PURCHASE_SLOT = 16; + private static final int ITEM_SLOT = 13; + + private final DataManager dataManager; + private final ShopManager shopManager; + private final int slot; + + public ConfirmGui(final TokenManagerPlugin plugin, final Shop shop, final int slot) { + super( + plugin, + shop, + InventoryUtil.deepCopyOf( + plugin.getShopConfig().getConfirmGuiSample(), + plugin.getConfiguration().getConfirmPurchaseTitle())); + this.dataManager = plugin.getDataManager(); + this.shopManager = plugin.getShopManager(); + this.slot = slot; + } + + @Override + public void refresh(final Player player, final boolean firstLoad) { + final long balance = dataManager.get(player).orElse(0); + final int cost = shop.getSlot(this.slot).getCost(); + inventory.setItem( + CONFIRM_PURCHASE_SLOT, + replace(player, inventory.getItem(CONFIRM_PURCHASE_SLOT), balance, cost)); + inventory.setItem( + ITEM_SLOT, replace(player, shop.getSlot(this.slot).getDisplayed().clone(), balance, cost)); + inventory.setItem( + CANCEL_PURCHASE_SLOT, + replace(player, inventory.getItem(CANCEL_PURCHASE_SLOT), balance, cost)); + } + + private ItemStack replace( + final Player player, final ItemStack item, final long balance, final int price) { + Placeholders.replace(item, price, "price"); + Placeholders.replace(item, balance, "tokens", "balance"); + Placeholders.replace(item, player.getName(), "player"); + return item; + } + + @Override + public boolean handle(final Player player, final int slot) { + if (slot == CONFIRM_PURCHASE_SLOT) { + boolean result = shop.getSlot(this.slot).purchase(player, false, shop.isAutoClose()); + refresh(player, false); + return result; + } + + if (slot == CANCEL_PURCHASE_SLOT) { + // Open back the parent shop. + shopManager.open(player, new ShopGui(plugin, shop)); + } + + return false; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/gui/guis/ShopGui.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/gui/guis/ShopGui.java new file mode 100644 index 00000000..666fde79 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/shop/gui/guis/ShopGui.java @@ -0,0 +1,106 @@ +package me.realized.tokenmanager.shop.gui.guis; + +import me.realized.tokenmanager.Permissions; +import me.realized.tokenmanager.TokenManagerPlugin; +import me.realized.tokenmanager.config.Lang; +import me.realized.tokenmanager.data.DataManager; +import me.realized.tokenmanager.shop.Shop; +import me.realized.tokenmanager.shop.Slot; +import me.realized.tokenmanager.shop.gui.BaseGui; +import me.realized.tokenmanager.util.Placeholders; +import me.realized.tokenmanager.util.compat.Items; +import me.realized.tokenmanager.util.inventory.InventoryUtil; +import me.realized.tokenmanager.util.inventory.ItemUtil; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; + +public class ShopGui extends BaseGui { + + private final Lang lang; + private final DataManager dataManager; + + public ShopGui(final TokenManagerPlugin plugin, final Shop shop) { + super(plugin, shop, Bukkit.createInventory(null, shop.getSize(), shop.getTitle())); + this.lang = plugin.getLang(); + this.dataManager = plugin.getDataManager(); + } + + @Override + public void refresh(final Player player, final boolean firstLoad) { + final long balance = dataManager.get(player).orElse(0); + + for (int slot = 0; slot < inventory.getSize(); slot++) { + final Slot data = shop.getSlot(slot); + + if (data == null) { + continue; + } + + ItemStack item = data.getDisplayed().clone(); + + if (Items.equals(data.getDisplayed(), Items.HEAD)) { + if (firstLoad) { + final SkullMeta skullMeta = (SkullMeta) item.getItemMeta(); + + if (skullMeta.getOwner() != null && skullMeta.getOwner().equals("%player%")) { + skullMeta.setOwner(player.getName()); + item.setItemMeta(skullMeta); + } + } else { + final ItemStack skull = inventory.getItem(slot); + ItemUtil.copyNameLore(item, skull); + item = skull; + } + } + + Placeholders.replace(item, balance, "tokens", "balance"); + Placeholders.replace(item, player.getName(), "player"); + inventory.setItem(slot, item); + } + } + + @Override + public boolean handle(final Player player, final int slot) { + final Slot data = shop.getSlot(slot); + + if (data == null) { + return false; + } + + final String slotInfo = shop.getName() + "-" + slot; + + if (!player.isOp() + && !player.hasPermission(Permissions.CMD_ADMIN) + && player.hasPermission(Permissions.SHOP_SLOT_CANCEL + slotInfo)) { + plugin.doSync(player::closeInventory); + lang.sendMessage(player, true, "ERROR.on-slot-cancel"); + return false; + } + + if (data.isUsePermission() + && !(player.hasPermission(Permissions.SHOP + slotInfo) + || player.hasPermission(Permissions.SHOP_SLOT_OLD + slotInfo))) { + plugin.doSync(player::closeInventory); + lang.sendMessage( + player, true, "ERROR.no-permission", "permission", Permissions.SHOP + slotInfo); + return false; + } + + if (data.getEmptySlotsRequired() > 0 + && InventoryUtil.getEmptySlots(player.getInventory()) < data.getEmptySlotsRequired()) { + plugin.doSync(player::closeInventory); + lang.sendMessage( + player, true, "ERROR.not-enough-space", "slots", data.getEmptySlotsRequired()); + return false; + } + + if (data.purchase(player, shop.isConfirmPurchase() || data.isConfirmPurchase(), false)) { + refresh(player, false); + return true; + } + + return false; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/EnumUtil.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/EnumUtil.java new file mode 100644 index 00000000..938fa89b --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/EnumUtil.java @@ -0,0 +1,22 @@ +package me.realized.tokenmanager.util; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public final class EnumUtil { + + private EnumUtil() {} + + public static E getByName(final String name, Class clazz) { + return clazz.cast( + Arrays.stream(clazz.getEnumConstants()) + .filter(type -> type.name().equalsIgnoreCase(name)) + .findFirst() + .orElse(null)); + } + + public static List getNames(final Class clazz) { + return Arrays.stream(clazz.getEnumConstants()).map(Enum::name).collect(Collectors.toList()); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Loadable.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Loadable.java new file mode 100644 index 00000000..bc03dc7f --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Loadable.java @@ -0,0 +1,8 @@ +package me.realized.tokenmanager.util; + +public interface Loadable { + + void handleLoad() throws Exception; + + void handleUnload() throws Exception; +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Log.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Log.java new file mode 100644 index 00000000..e8a495cf --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Log.java @@ -0,0 +1,37 @@ +package me.realized.tokenmanager.util; + +import java.util.logging.Level; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.plugin.java.JavaPlugin; + +public final class Log { + + private static JavaPlugin source; + + private Log() {} + + public static void setSource(final JavaPlugin plugin) { + source = plugin; + } + + private static void log(final Level level, final String s) { + if (source != null) { + source.getLogger().log(level, s); + } + } + + public static void info(final String s) { + log(Level.INFO, s); + } + + public static void error(final String s) { + if (source != null) { + Bukkit.getConsoleSender().sendMessage("[" + source.getName() + "] " + ChatColor.RED + s); + } + } + + public static void error(final Loadable loadable, final String s) { + error(loadable.getClass().getSimpleName() + ": " + s); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/NumberUtil.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/NumberUtil.java new file mode 100644 index 00000000..3fb2688c --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/NumberUtil.java @@ -0,0 +1,90 @@ +package me.realized.tokenmanager.util; + +import java.text.DecimalFormat; +import java.util.OptionalLong; + +public final class NumberUtil { + + private static final DecimalFormat COMMA_FORMAT = new DecimalFormat("#,###"); + + /** + * Copy of {@link Long#parseLong(String)} but returns an empty {@link OptionalLong} instead of + * throwing a {@link NumberFormatException}. + * + * @param s String to parse. + * @return {@link OptionalLong} instance with parsed value inside or empty if string is invalid. + */ + public static OptionalLong parseLong(final String s) throws NumberFormatException { + if (s == null) { + return OptionalLong.empty(); + } + + long result = 0; + boolean negative = false; + int i = 0, len = s.length(); + long limit = -Long.MAX_VALUE; + long multmin; + int digit; + + if (len > 0) { + char firstChar = s.charAt(0); + if (firstChar < '0') { + if (firstChar == '-') { + negative = true; + limit = Long.MIN_VALUE; + } else if (firstChar != '+') { + return OptionalLong.empty(); + } + + if (len == 1) { + return OptionalLong.empty(); + } + + i++; + } + multmin = limit / 10; + while (i < len) { + digit = Character.digit(s.charAt(i++), 10); + + if (digit < 0) { + return OptionalLong.empty(); + } + if (result < multmin) { + return OptionalLong.empty(); + } + result *= 10; + if (result < limit + digit) { + return OptionalLong.empty(); + } + result -= digit; + } + } else { + return OptionalLong.empty(); + } + + return OptionalLong.of(negative ? result : -result); + } + + public static String withCommas(final long n) { + return COMMA_FORMAT.format(n); + } + + // Source: + // https://stackoverflow.com/questions/9769554/how-to-convert-number-into-k-thousands-m-million-and-b-billion-suffix-in-jsp + public static String withSuffix(final long n) { + if (n < 1000) { + return "" + n; + } + + final int exp = (int) (Math.log(n) / Math.log(1000)); + return String.format("%.1f%c", n / Math.pow(1000, exp), "kMBTQ".charAt(exp - 1)); + } + + public static boolean isLower(String version, String otherVersion) { + version = version.replace("-SNAPSHOT", "").replace(".", ""); + otherVersion = otherVersion.replace("-SNAPSHOT", "").replace(".", ""); + return NumberUtil.parseLong(version).orElse(0) < NumberUtil.parseLong(otherVersion).orElse(0); + } + + private NumberUtil() {} +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Placeholders.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Placeholders.java new file mode 100644 index 00000000..5752f200 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Placeholders.java @@ -0,0 +1,61 @@ +package me.realized.tokenmanager.util; + +import java.util.List; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +public final class Placeholders { + + public static void replace( + final ItemStack item, final Object value, final String... placeholders) { + if (!item.hasItemMeta()) { + return; + } + + final ItemMeta meta = item.getItemMeta(); + + if (meta.hasLore()) { + final List lore = meta.getLore(); + lore.replaceAll( + line -> { + for (final String placeholder : placeholders) { + line = + value instanceof Number + ? replace(line, (Number) value, placeholder) + : line.replace("%" + placeholder + "%", String.valueOf(value)); + } + + return line; + }); + meta.setLore(lore); + } + + if (meta.hasDisplayName()) { + String displayName = meta.getDisplayName(); + + for (final String placeholder : placeholders) { + displayName = + value instanceof Number + ? replace(displayName, (Number) value, placeholder) + : displayName.replace("%" + placeholder + "%", String.valueOf(value)); + } + + meta.setDisplayName(displayName); + } + + item.setItemMeta(meta); + } + + public static String replace(String line, final Number value, final String... placeholders) { + for (final String key : placeholders) { + line = + line.replace("%" + key + "%", String.valueOf(value)) + .replace("%" + key + "_raw%", String.valueOf(value)) + .replace("%" + key + "_commas%", NumberUtil.withCommas(value.longValue())) + .replace("%" + key + "_formatted%", NumberUtil.withSuffix(value.longValue())); + } + return line; + } + + private Placeholders() {} +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Reloadable.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Reloadable.java new file mode 100644 index 00000000..d9b07ac5 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/Reloadable.java @@ -0,0 +1,3 @@ +package me.realized.tokenmanager.util; + +public interface Reloadable {} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/StringUtil.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/StringUtil.java new file mode 100644 index 00000000..554f65a8 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/StringUtil.java @@ -0,0 +1,86 @@ +package me.realized.tokenmanager.util; + +import java.util.List; +import java.util.function.Function; +import org.bukkit.ChatColor; + +public final class StringUtil { + + private StringUtil() {} + + public static String fromList(final List list) { + StringBuilder builder = new StringBuilder(); + + if (list != null && !list.isEmpty()) { + for (int i = 0; i < list.size(); i++) { + builder.append(list.get(i).toString()).append(i + 1 != list.size() ? "\n" : ""); + } + } + + return builder.toString(); + } + + public static String color(final String input) { + return ChatColor.translateAlternateColorCodes('&', input); + } + + public static List color(final List input) { + return color(input, null); + } + + public static List color(final List input, final Function extra) { + input.replaceAll(s -> s = color(extra != null ? extra.apply(s) : s)); + return input; + } + + public static String format(long seconds) { + if (seconds <= 0) { + return "updating..."; + } + + long years = seconds / 31556952; + seconds -= years * 31556952; + long months = seconds / 2592000; + seconds -= months * 2592000; + long weeks = seconds / 604800; + seconds -= weeks * 604800; + long days = seconds / 86400; + seconds -= days * 86400; + long hours = seconds / 3600; + seconds -= hours * 3600; + long minutes = seconds / 60; + seconds -= minutes * 60; + + StringBuilder sb = new StringBuilder(); + + if (years > 0) { + sb.append(years).append("yr"); + } + + if (months > 0) { + sb.append(months).append("mo"); + } + + if (weeks > 0) { + sb.append(weeks).append("w"); + } + + if (days > 0) { + sb.append(days).append("d"); + } + + if (hours > 0) { + sb.append(hours).append("h"); + } + + if (minutes > 0) { + sb.append(minutes).append("m"); + } + + if (seconds > 0) { + sb.append(seconds).append("s"); + } + + return sb.toString(); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/UpdateChecker.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/UpdateChecker.java new file mode 100644 index 00000000..59ae9060 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/UpdateChecker.java @@ -0,0 +1,46 @@ +package me.realized.tokenmanager.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.function.BiConsumer; +import org.bukkit.plugin.Plugin; + +public final class UpdateChecker { + + private static final String API_URL = "https://api.spigotmc.org/legacy/update.php?resource=%s"; + + private final Plugin plugin; + private final int id; + + public UpdateChecker(final Plugin plugin, final int id) { + this.plugin = plugin; + this.id = id; + } + + public void check(final BiConsumer callback) { + final String currentVersion = plugin.getDescription().getVersion(); + + plugin + .getServer() + .getScheduler() + .runTaskAsynchronously( + plugin, + () -> { + try (BufferedReader reader = + new BufferedReader( + new InputStreamReader(new URL(String.format(API_URL, id)).openStream()))) { + final String latestVersion = reader.readLine(); + + if (latestVersion == null) { + return; + } + + final boolean updateAvailable = NumberUtil.isLower(currentVersion, latestVersion); + callback.accept(updateAvailable, updateAvailable ? latestVersion : currentVersion); + } catch (IOException ignored) { + } + }); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/command/AbstractCommand.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/command/AbstractCommand.java new file mode 100644 index 00000000..bf53bc9d --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/command/AbstractCommand.java @@ -0,0 +1,180 @@ +package me.realized.tokenmanager.util.command; + +import com.google.common.collect.Lists; +import java.text.MessageFormat; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.Getter; +import me.realized.tokenmanager.util.StringUtil; +import org.bukkit.command.CommandSender; +import org.bukkit.command.PluginCommand; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +public abstract class AbstractCommand

implements TabCompleter { + + protected final P plugin; + + @Getter private final String name; + @Getter private final String usage; + @Getter private final String permission; + @Getter private final boolean playerOnly; + @Getter private final int length; + @Getter private final List aliases; + + private Map> children; + + public AbstractCommand( + final P plugin, + final String name, + final String usage, + final String permission, + final int length, + final boolean playerOnly, + final String... aliases) { + this.plugin = plugin; + this.name = name; + this.usage = usage; + this.permission = permission; + this.length = length; + this.playerOnly = playerOnly; + + final List names = Lists.newArrayList(aliases); + names.add(name); + this.aliases = Collections.unmodifiableList(names); + } + + @SafeVarargs + protected final void child(final AbstractCommand

... commands) { + if (commands == null || commands.length == 0) { + return; + } + + if (children == null) { + children = new HashMap<>(); + } + + for (final AbstractCommand

child : commands) { + for (final String alias : child.aliases) { + children.put(alias.toLowerCase(), child); + } + } + } + + protected void handleMessage( + final CommandSender sender, final MessageType type, final String... args) { + sender.sendMessage(type.defaultMessage.format(args)); + } + + private PluginCommand getCommand() { + PluginCommand pluginCommand = plugin.getCommand(name); + + if (pluginCommand == null) { + throw new IllegalArgumentException("Command is not registered in plugin.yml"); + } + + return pluginCommand; + } + + public final void register() { + final PluginCommand pluginCommand = getCommand(); + + pluginCommand.setExecutor( + (sender, command, label, args) -> { + if (playerOnly && !(sender instanceof Player)) { + handleMessage(sender, MessageType.PLAYER_ONLY); + return true; + } + + if (permission != null && !sender.hasPermission(permission)) { + handleMessage(sender, MessageType.NO_PERMISSION, permission); + return true; + } + + if (args.length > 0 && children != null) { + final AbstractCommand

child = children.get(args[0].toLowerCase()); + + if (child == null) { + handleMessage(sender, MessageType.SUB_COMMAND_INVALID, label, args[0]); + return true; + } + + if (child.playerOnly && !(sender instanceof Player)) { + handleMessage(sender, MessageType.PLAYER_ONLY); + return true; + } + + if (child.permission != null && !sender.hasPermission(child.permission)) { + handleMessage(sender, MessageType.NO_PERMISSION, child.permission); + return true; + } + + if (args.length < child.length) { + handleMessage(sender, MessageType.SUB_COMMAND_USAGE, label, child.usage); + return true; + } + + child.execute(sender, label, args); + return true; + } + + execute(sender, label, args); + return true; + }); + pluginCommand.setTabCompleter( + (sender, command, alias, args) -> { + if (children != null && args.length > 1) { + final AbstractCommand

child = children.get(args[0].toLowerCase()); + + if (child != null) { + final List result = child.onTabComplete(sender, command, alias, args); + + if (result != null) { + return result; + } + } + } + + return onTabComplete(sender, command, alias, args); + }); + } + + @Override + public List onTabComplete( + CommandSender sender, org.bukkit.command.Command command, String alias, String[] args) { + if (args.length == 0) { + return null; + } + + if (args.length == 1 && children != null) { + return children.values().stream() + .filter(child -> child.name.startsWith(args[0].toLowerCase())) + .map(child -> child.name) + .distinct() + .sorted(String::compareTo) + .collect(Collectors.toList()); + } + + return null; + } + + protected abstract void execute( + final CommandSender sender, final String label, final String[] args); + + protected enum MessageType { + PLAYER_ONLY("&cThis command can only be executed by a player!"), + NO_PERMISSION("&cYou need the following permission: {0}"), + SUB_COMMAND_INVALID("&c''{1}'' is not a valid sub command. Type /{0} for help."), + SUB_COMMAND_USAGE("&cUsage: /{0} {1}"); + + private final MessageFormat defaultMessage; + + MessageType(final String defaultMessage) { + this.defaultMessage = new MessageFormat(StringUtil.color(defaultMessage)); + } + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/CompatBase.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/CompatBase.java new file mode 100644 index 00000000..0249a220 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/CompatBase.java @@ -0,0 +1,50 @@ +package me.realized.tokenmanager.util.compat; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.UUID; +import org.bukkit.inventory.ItemStack; + +class CompatBase { + + static final Method AS_NMS_COPY, AS_BUKKIT_COPY; + + static final Class TAG_COMPOUND; + static final Method GET_TAG, SET_TAG, SET, SET_STRING; + static final Constructor GAME_PROFILE_CONST, PROPERTY_CONST; + static final Method GET_PROPERTIES, PUT; + static final Field PROFILE; + + static { + final Class CB_ITEMSTACK = ReflectionUtil.getCBClass("inventory.CraftItemStack"); + final Class NMS_ITEMSTACK = + ReflectionUtil.getNMSClass( + (ReflectionUtil.getMajorVersion() >= 17 ? "world.item." : "") + "ItemStack"); + AS_NMS_COPY = ReflectionUtil.getMethod(CB_ITEMSTACK, "asNMSCopy", ItemStack.class); + AS_BUKKIT_COPY = ReflectionUtil.getMethod(CB_ITEMSTACK, "asBukkitCopy", NMS_ITEMSTACK); + TAG_COMPOUND = + ReflectionUtil.getNMSClass( + (ReflectionUtil.getMajorVersion() >= 17 ? "nbt." : "") + "NBTTagCompound"); + + final Class TAG_BASE = + ReflectionUtil.getNMSClass( + (ReflectionUtil.getMajorVersion() >= 17 ? "nbt." : "") + "NBTBase"); + GET_TAG = ReflectionUtil.getMethod(NMS_ITEMSTACK, "getTag"); + SET_TAG = ReflectionUtil.getMethod(NMS_ITEMSTACK, "setTag", TAG_COMPOUND); + SET = ReflectionUtil.getMethod(TAG_COMPOUND, "set", String.class, TAG_BASE); + SET_STRING = ReflectionUtil.getMethod(TAG_COMPOUND, "setString", String.class, String.class); + + final Class GAME_PROFILE = ReflectionUtil.getALClass("GameProfile"); + GAME_PROFILE_CONST = ReflectionUtil.getConstructor(GAME_PROFILE, UUID.class, String.class); + GET_PROPERTIES = ReflectionUtil.getMethod(GAME_PROFILE, "getProperties"); + final Class PROPERTY = ReflectionUtil.getALClass("properties.Property"); + PROPERTY_CONST = ReflectionUtil.getConstructor(PROPERTY, String.class, String.class); + PUT = + ReflectionUtil.getMethod( + ReflectionUtil.getALClass("properties.PropertyMap"), "put", Object.class, Object.class); + PROFILE = + ReflectionUtil.getDeclaredField( + ReflectionUtil.getCBClass("inventory.CraftMetaSkull"), "profile"); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/CompatUtil.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/CompatUtil.java new file mode 100644 index 00000000..56f7db61 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/CompatUtil.java @@ -0,0 +1,23 @@ +package me.realized.tokenmanager.util.compat; + +public final class CompatUtil { + public static boolean isPre1_17() { + return false; + } + + public static boolean isPre1_14() { + return false; + } + + public static boolean isPre1_13() { + return false; + } + + public static boolean isPre1_12() { + return false; + } + + public static boolean isPre1_9() { + return false; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Items.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Items.java new file mode 100644 index 00000000..55258270 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Items.java @@ -0,0 +1,48 @@ +package me.realized.tokenmanager.util.compat; + +import me.realized.tokenmanager.util.inventory.ItemBuilder; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +public final class Items { + + private static final String PANE = "STAINED_GLASS_PANE"; + + public static final ItemStack RED_PANE; + public static final ItemStack GRAY_PANE; + public static final ItemStack GREEN_PANE; + + public static final ItemStack HEAD; + + static { + RED_PANE = + (CompatUtil.isPre1_13() + ? ItemBuilder.of(PANE, 1, (short) 14) + : ItemBuilder.of(Material.RED_STAINED_GLASS_PANE)) + .name(" ") + .build(); + GRAY_PANE = + (CompatUtil.isPre1_13() + ? ItemBuilder.of(PANE, 1, (short) 7) + : ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE)) + .name(" ") + .build(); + GREEN_PANE = + (CompatUtil.isPre1_13() + ? ItemBuilder.of(PANE, 1, (short) 13) + : ItemBuilder.of(Material.GREEN_STAINED_GLASS_PANE)) + .name(" ") + .build(); + HEAD = + (CompatUtil.isPre1_13() + ? ItemBuilder.of("SKULL_ITEM", 1, (short) 3) + : ItemBuilder.of(Material.PLAYER_HEAD)) + .build(); + } + + public static boolean equals(final ItemStack item, final ItemStack other) { + return item.getType() == other.getType() && item.getDurability() == other.getDurability(); + } + + private Items() {} +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/ReflectionUtil.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/ReflectionUtil.java new file mode 100644 index 00000000..b6d64879 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/ReflectionUtil.java @@ -0,0 +1,152 @@ +package me.realized.tokenmanager.util.compat; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import me.realized.tokenmanager.util.NumberUtil; +import org.bukkit.Bukkit; + +public final class ReflectionUtil { + private static final String PACKAGE_VERSION; + private static final int MAJOR_VERSION; + + static { + final String packageName = Bukkit.getServer().getClass().getPackage().getName(); + PACKAGE_VERSION = packageName.substring(packageName.lastIndexOf('.') + 1); + MAJOR_VERSION = (int) NumberUtil.parseLong(PACKAGE_VERSION.split("_")[1]).orElse(0); + } + + public static int getMajorVersion() { + return MAJOR_VERSION; + } + + public static Class getClassUnsafe(final String name) { + try { + return Class.forName(name); + } catch (ClassNotFoundException ex) { + return null; + } + } + + public static Method getMethodUnsafe( + final Class clazz, final String name, final Class... parameters) { + try { + return clazz.getMethod(name, parameters); + } catch (NoSuchMethodException ex) { + return null; + } + } + + public static Class getNMSClass(final String name, final boolean logError) { + try { + return Class.forName( + "net.minecraft" + + (getMajorVersion() < 17 ? (".server." + PACKAGE_VERSION) : "") + + "." + + name); + } catch (ClassNotFoundException ex) { + if (logError) { + ex.printStackTrace(); + } + + return null; + } + } + + public static Class getNMSClass(final String name) { + return getNMSClass(name, true); + } + + public static Class getCBClass(final String path, final boolean logError) { + try { + return Class.forName("org.bukkit.craftbukkit." + PACKAGE_VERSION + "." + path); + } catch (ClassNotFoundException ex) { + if (logError) { + ex.printStackTrace(); + } + + return null; + } + } + + public static Class getCBClass(final String path) { + return getCBClass(path, true); + } + + public static Class getALClass(final String name) { + try { + return Class.forName("com.mojang.authlib." + name); + } catch (ClassNotFoundException ex) { + ex.printStackTrace(); + return null; + } + } + + public static Method getMethod( + final Class clazz, final String name, final Class... parameters) { + try { + return clazz.getMethod(name, parameters); + } catch (NoSuchMethodException ex) { + ex.printStackTrace(); + return null; + } + } + + private static Method findDeclaredMethod( + final Class clazz, final String name, final Class... parameters) + throws NoSuchMethodException { + final Method method = clazz.getDeclaredMethod(name, parameters); + method.setAccessible(true); + return method; + } + + public static Method getDeclaredMethod( + final Class clazz, final String name, final Class... parameters) { + try { + return findDeclaredMethod(clazz, name, parameters); + } catch (NoSuchMethodException ex) { + ex.printStackTrace(); + return null; + } + } + + public static Method getDeclaredMethodUnsafe( + final Class clazz, final String name, final Class... parameters) { + try { + return findDeclaredMethod(clazz, name, parameters); + } catch (NoSuchMethodException ex) { + return null; + } + } + + public static Field getField(final Class clazz, final String name) { + try { + return clazz.getField(name); + } catch (NoSuchFieldException ex) { + ex.printStackTrace(); + return null; + } + } + + public static Field getDeclaredField(final Class clazz, final String name) { + try { + final Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + return field; + } catch (NoSuchFieldException ex) { + ex.printStackTrace(); + return null; + } + } + + public static Constructor getConstructor(final Class clazz, final Class... parameters) { + try { + return clazz.getConstructor(parameters); + } catch (NoSuchMethodException ex) { + ex.printStackTrace(); + return null; + } + } + + private ReflectionUtil() {} +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Skulls.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Skulls.java new file mode 100644 index 00000000..65db7531 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Skulls.java @@ -0,0 +1,26 @@ +package me.realized.tokenmanager.util.compat; + +import java.lang.reflect.InvocationTargetException; +import java.util.UUID; +import org.bukkit.inventory.meta.SkullMeta; + +public final class Skulls extends CompatBase { + + public static void setSkull(final SkullMeta meta, final String value) { + try { + final Object profile = GAME_PROFILE_CONST.newInstance(UUID.randomUUID(), null); + final Object propertyMap = GET_PROPERTIES.invoke(profile); + + if (propertyMap == null) { + throw new IllegalStateException("Profile doesn't contain a property map"); + } + + PUT.invoke(propertyMap, "textures", PROPERTY_CONST.newInstance("textures", value)); + PROFILE.set(meta, profile); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException ex) { + ex.printStackTrace(); + } + } + + private Skulls() {} +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/SpawnEggs.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/SpawnEggs.java new file mode 100644 index 00000000..da26100c --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/SpawnEggs.java @@ -0,0 +1,38 @@ +package me.realized.tokenmanager.util.compat; + +import lombok.Getter; +import org.bukkit.Material; +import org.bukkit.entity.EntityType; +import org.bukkit.inventory.ItemStack; + +public class SpawnEggs extends CompatBase { + + @Getter private final EntityType type; + + public SpawnEggs(EntityType type) { + this.type = type; + } + + @SuppressWarnings("deprecation") + public ItemStack toItemStack() { + try { + final ItemStack item = new ItemStack(Material.getMaterial("MONSTER_EGG")); + Object nmsItem = AS_NMS_COPY.invoke(null, item); + Object tag = GET_TAG.invoke(nmsItem); + + if (tag == null) { + tag = TAG_COMPOUND.newInstance(); + } + + final Object id = TAG_COMPOUND.newInstance(); + SET_STRING.invoke(id, "id", type.getName()); + SET.invoke(tag, "EntityTag", id); + SET_TAG.invoke(nmsItem, tag); + return (ItemStack) AS_BUKKIT_COPY.invoke(null, nmsItem); + } catch (Exception ex) { + ex.printStackTrace(); + } + + return null; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Terracottas.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Terracottas.java new file mode 100644 index 00000000..a53ad3d4 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/compat/Terracottas.java @@ -0,0 +1,35 @@ +package me.realized.tokenmanager.util.compat; + +import java.util.HashMap; +import java.util.Map; +import org.bukkit.Material; + +public final class Terracottas { + + private static final Map DATA_TO_TERRACOTTA = new HashMap<>(); + + static { + DATA_TO_TERRACOTTA.put((short) 0, "WHITE_TERRACOTTA"); + DATA_TO_TERRACOTTA.put((short) 1, "ORANGE_TERRACOTTA"); + DATA_TO_TERRACOTTA.put((short) 2, "MAGENTA_TERRACOTTA"); + DATA_TO_TERRACOTTA.put((short) 3, "LIGHT_BLUE_TERRACOTTA"); + DATA_TO_TERRACOTTA.put((short) 4, "YELLOW_TERRACOTTA"); + DATA_TO_TERRACOTTA.put((short) 5, "LIME_TERRACOTTA"); + DATA_TO_TERRACOTTA.put((short) 6, "PINK_TERRACOTTA"); + DATA_TO_TERRACOTTA.put((short) 7, "GRAY_TERRACOTTA"); + DATA_TO_TERRACOTTA.put((short) 8, "LIGHT_GRAY_TERRACOTTA"); + DATA_TO_TERRACOTTA.put((short) 9, "CYAN_TERRACOTTA"); + DATA_TO_TERRACOTTA.put((short) 10, "PURPLE_TERRACOTTA"); + DATA_TO_TERRACOTTA.put((short) 11, "BLUE_TERRACOTTA"); + DATA_TO_TERRACOTTA.put((short) 12, "BROWN_TERRACOTTA"); + DATA_TO_TERRACOTTA.put((short) 13, "GREEN_TERRACOTTA"); + DATA_TO_TERRACOTTA.put((short) 14, "RED_TERRACOTTA"); + DATA_TO_TERRACOTTA.put((short) 15, "BLACK_TERRACOTTA"); + } + + public static Material from(final short data) { + return Material.getMaterial(DATA_TO_TERRACOTTA.get(data)); + } + + private Terracottas() {} +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/config/AbstractConfiguration.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/config/AbstractConfiguration.java new file mode 100644 index 00000000..c34ecf3d --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/config/AbstractConfiguration.java @@ -0,0 +1,209 @@ +package me.realized.tokenmanager.util.config; + +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import me.realized.tokenmanager.util.Loadable; +import me.realized.tokenmanager.util.config.convert.Converter; +import org.bukkit.configuration.MemorySection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +public abstract class AbstractConfiguration

implements Loadable { + + private static final String CONVERT_START = + "[!] Converting your current configuration (%s) to the new version..."; + private static final String CONVERT_SAVE = "[!] Your old configuration was stored as %s."; + private static final String CONVERT_DONE = "[!] Conversion complete!"; + + private static final Pattern KEY_PATTERN = Pattern.compile("^([ ]*)([^ \"]+)[:].*$"); + private static final Pattern COMMENT_PATTERN = Pattern.compile("^([ ]*[#].*)|[ ]*$"); + + protected final P plugin; + + private final String name; + private final File file; + + private FileConfiguration configuration; + + public AbstractConfiguration(final P plugin, final String name) { + this.plugin = plugin; + this.name = name + ".yml"; + this.file = new File(plugin.getDataFolder(), this.name); + } + + @Override + public void handleLoad() throws Exception { + if (!file.exists()) { + plugin.saveResource(name, true); + } + + loadValues(configuration = YamlConfiguration.loadConfiguration(file)); + } + + @Override + public void handleUnload() {} + + protected abstract void loadValues(final FileConfiguration configuration) throws Exception; + + protected int getLatestVersion() throws Exception { + final InputStream stream = plugin.getClass().getResourceAsStream("/" + name); + + if (stream == null) { + throw new IllegalStateException( + plugin.getName() + + "'s jar file was replaced, but a reload was called! Please restart your server instead when updating this plugin."); + } + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { + return YamlConfiguration.loadConfiguration(reader).getInt("config-version", -1); + } + } + + protected FileConfiguration convert(final Converter converter) throws IOException { + plugin.getLogger().info(String.format(CONVERT_START, name)); + + final Map oldValues = new HashMap<>(); + + for (final String key : configuration.getKeys(true)) { + if (key.equals("config-version")) { + continue; + } + + final Object value = configuration.get(key); + + if (value instanceof MemorySection) { + continue; + } + + oldValues.put(key, value); + } + + if (converter != null) { + converter + .renamedKeys() + .forEach( + (old, changed) -> { + final Object previous = oldValues.get(old); + + if (previous != null) { + oldValues.remove(old); + oldValues.put(changed, previous); + } + }); + } + + final String newName = name.replace(".yml", "") + "-" + System.currentTimeMillis() + ".yml"; + final File copied = + Files.copy(file.toPath(), new File(plugin.getDataFolder(), newName).toPath()).toFile(); + plugin.getLogger().info(String.format(CONVERT_SAVE, copied.getName())); + plugin.saveResource(name, true); + + // Loads comments of the new configuration file + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + final Multimap> comments = LinkedListMultimap.create(); + final List currentComments = new ArrayList<>(); + + String line; + Matcher matcher; + + while ((line = reader.readLine()) != null) { + if ((matcher = KEY_PATTERN.matcher(line)).find() + && !COMMENT_PATTERN.matcher(line).matches()) { + comments.put(matcher.group(2), Lists.newArrayList(currentComments)); + currentComments.clear(); + } else if (COMMENT_PATTERN.matcher(line).matches()) { + currentComments.add(line); + } + } + + configuration = YamlConfiguration.loadConfiguration(file); + configuration.options().header(null); + + // Transfer values from the old configuration + for (Map.Entry entry : oldValues.entrySet()) { + final String key = entry.getKey(); + final Object value = configuration.get(key); + + if ((value != null && !(value instanceof MemorySection)) + || transferredSections().stream().anyMatch(section -> key.startsWith(section + "."))) { + configuration.set(key, entry.getValue()); + } + } + + final List commentlessData = + Lists.newArrayList(configuration.saveToString().split("\n")); + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + for (final String data : commentlessData) { + matcher = KEY_PATTERN.matcher(data); + + if (matcher.find()) { + final String key = matcher.group(2); + final Collection> result = comments.get(key); + + if (result != null) { + final List> commentData = Lists.newArrayList(result); + + if (!commentData.isEmpty()) { + for (final String comment : commentData.get(0)) { + writer.write(comment); + writer.newLine(); + } + + commentData.remove(0); + comments.replaceValues(key, commentData); + } + } + } + + writer.write(data); + + if (commentlessData.indexOf(data) + 1 < commentlessData.size()) { + writer.newLine(); + } else if (!currentComments.isEmpty()) { + writer.newLine(); + } + } + + // Handles comments at the end of the file without any key + for (final String comment : currentComments) { + writer.write(comment); + + if (currentComments.indexOf(comment) + 1 < currentComments.size()) { + writer.newLine(); + } + } + + writer.flush(); + } + + plugin.getLogger().info(CONVERT_DONE); + } + + return configuration; + } + + protected Set transferredSections() { + return Collections.emptySet(); + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/config/convert/Converter.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/config/convert/Converter.java new file mode 100644 index 00000000..290c3146 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/config/convert/Converter.java @@ -0,0 +1,8 @@ +package me.realized.tokenmanager.util.config.convert; + +import java.util.Map; + +public interface Converter { + + Map renamedKeys(); +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/hook/AbstractHookManager.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/hook/AbstractHookManager.java new file mode 100644 index 00000000..d0630620 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/hook/AbstractHookManager.java @@ -0,0 +1,53 @@ +package me.realized.tokenmanager.util.hook; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; +import me.realized.tokenmanager.util.Loadable; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +public abstract class AbstractHookManager

implements Loadable { + + protected final P plugin; + private final Map>, PluginHook

> hooks = new HashMap<>(); + + public AbstractHookManager(final P plugin) { + this.plugin = plugin; + } + + protected boolean register(final String name, final Class> clazz) { + final Plugin target = Bukkit.getPluginManager().getPlugin(name); + + if (target == null || !target.isEnabled()) { + return false; + } + + try { + final Constructor> constructor = + clazz.getConstructor(plugin.getClass()); + final boolean result; + + if (result = + constructor != null + && hooks.putIfAbsent(clazz, constructor.newInstance(plugin)) == null) { + plugin.getLogger().info("Successfully hooked into '" + name + "'!"); + } + + return result; + } catch (NoSuchMethodException + | IllegalAccessException + | InstantiationException + | InvocationTargetException ex) { + plugin.getLogger().warning("Failed to hook into " + name + ": " + ex.getMessage()); + } + + return false; + } + + public > T getHook(Class clazz) { + return clazz != null ? clazz.cast(hooks.get(clazz)) : null; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/hook/PluginHook.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/hook/PluginHook.java new file mode 100644 index 00000000..ba0397a6 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/hook/PluginHook.java @@ -0,0 +1,19 @@ +package me.realized.tokenmanager.util.hook; + +import org.bukkit.plugin.java.JavaPlugin; + +public class PluginHook

{ + + protected final P plugin; + + private final String name; + + public PluginHook(final P plugin, final String name) { + this.plugin = plugin; + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/GUIBuilder.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/GUIBuilder.java new file mode 100644 index 00000000..b4022048 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/GUIBuilder.java @@ -0,0 +1,98 @@ +package me.realized.tokenmanager.util.inventory; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +public final class GUIBuilder { + + private final Inventory gui; + + private GUIBuilder(final String title, final int rows) { + this.gui = Bukkit.createInventory(null, rows * 9, title); + } + + public static GUIBuilder of(final String title, final int rows) { + return new GUIBuilder(title, rows); + } + + public GUIBuilder set(final int slot, final ItemStack item) { + gui.setItem(slot, item); + return this; + } + + public GUIBuilder fill(final ItemStack item, final int... slots) { + for (final int slot : slots) { + gui.setItem(slot, item); + } + + return this; + } + + public GUIBuilder fillRange(final int from, final int to, final ItemStack item) { + for (int slot = from; slot < to; slot++) { + gui.setItem(slot, item); + } + + return this; + } + + public GUIBuilder fillEmpty(final ItemStack item) { + for (int slot = 0; slot < gui.getSize(); slot++) { + final ItemStack target = gui.getItem(slot); + + if (target != null && target.getType() != Material.AIR) { + gui.setItem(slot, item); + } + } + + return this; + } + + public GUIBuilder pattern(final Pattern pattern) { + pattern.apply(gui); + return this; + } + + public Inventory build() { + return gui; + } + + public static final class Pattern { + + private final List rows; + private final Map keys = new HashMap<>(); + + private Pattern(final String... rows) { + this.rows = Arrays.asList(rows); + } + + public static Pattern of(final String... rows) { + return new Pattern(rows); + } + + public Pattern specify(final char key, final ItemStack item) { + keys.put(key, item); + return this; + } + + private void apply(final Inventory inventory) { + for (int row = 0; row < rows.size(); row++) { + final String pattern = rows.get(row); + + for (int i = 0; i < pattern.length(); i++) { + if (i > 8) { + break; + } + + inventory.setItem(row * 9 + i, keys.get(pattern.charAt(i)).clone()); + } + } + } + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/InventoryUtil.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/InventoryUtil.java new file mode 100644 index 00000000..abc1f66d --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/InventoryUtil.java @@ -0,0 +1,50 @@ +package me.realized.tokenmanager.util.inventory; + +import me.realized.tokenmanager.util.StringUtil; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; + +public final class InventoryUtil { + + private InventoryUtil() {} + + public static Inventory deepCopyOf(final Inventory inventory, final String title) { + final Inventory result = + Bukkit.createInventory(null, inventory.getSize(), StringUtil.color(title)); + + for (int i = 0; i < inventory.getSize(); i++) { + result.setItem(i, inventory.getItem(i).clone()); + } + + return result; + } + + public static int getEmptySlots(final Inventory inventory) { + int empty = 0; + + for (int i = 0; i < 36; i++) { + final ItemStack item = inventory.getItem(i); + + if (item == null) { + empty++; + } + } + + return empty; + } + + public static boolean isInventoryFull(final Player player) { + return player.getInventory().firstEmpty() == -1; + } + + public static Inventory getClickedInventory(final int rawSlot, final InventoryView view) { + if (rawSlot < view.getTopInventory().getSize()) { + return view.getTopInventory(); + } else { + return view.getBottomInventory(); + } + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/ItemBuilder.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/ItemBuilder.java new file mode 100644 index 00000000..6c2194f2 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/ItemBuilder.java @@ -0,0 +1,55 @@ +package me.realized.tokenmanager.util.inventory; + +import java.util.Arrays; +import java.util.List; +import me.realized.tokenmanager.util.StringUtil; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +public final class ItemBuilder { + + private final ItemStack result; + + private ItemBuilder(final Material type, final int amount, final short durability) { + this.result = new ItemStack(type, amount, durability); + } + + public static ItemBuilder of(final Material type) { + return of(type, 1); + } + + public static ItemBuilder of(final Material type, final int amount) { + return of(type, amount, (short) 0); + } + + public static ItemBuilder of(final Material type, final int amount, final short durability) { + return new ItemBuilder(type, amount, durability); + } + + public static ItemBuilder of(final String type, final int amount, final short durability) { + return new ItemBuilder(Material.getMaterial(type), amount, durability); + } + + public ItemBuilder name(final String name) { + ItemMeta meta = result.getItemMeta(); + meta.setDisplayName(StringUtil.color(name)); + result.setItemMeta(meta); + return this; + } + + public ItemBuilder lore(final String... lore) { + return lore(Arrays.asList(lore)); + } + + public ItemBuilder lore(final List lore) { + ItemMeta meta = result.getItemMeta(); + meta.setLore(StringUtil.color(lore)); + result.setItemMeta(meta); + return this; + } + + public ItemStack build() { + return result; + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/ItemUtil.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/ItemUtil.java new file mode 100644 index 00000000..7cf6c554 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/inventory/ItemUtil.java @@ -0,0 +1,335 @@ +package me.realized.tokenmanager.util.inventory; + +import com.google.common.collect.Lists; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.OptionalLong; +import java.util.function.Consumer; +import me.realized.tokenmanager.util.EnumUtil; +import me.realized.tokenmanager.util.NumberUtil; +import me.realized.tokenmanager.util.StringUtil; +import me.realized.tokenmanager.util.compat.CompatUtil; +import me.realized.tokenmanager.util.compat.Items; +import me.realized.tokenmanager.util.compat.Skulls; +import me.realized.tokenmanager.util.compat.SpawnEggs; +import me.realized.tokenmanager.util.compat.Terracottas; +import org.bukkit.Color; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.EntityType; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.LeatherArmorMeta; +import org.bukkit.inventory.meta.PotionMeta; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.potion.PotionData; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.potion.PotionType; + +public final class ItemUtil { + + private static final Map ENCHANTMENTS; + private static final Map EFFECTS; + + static { + final Map enchantments = new HashMap<>(); + Arrays.stream(Enchantment.values()) + .forEach( + enchantment -> { + enchantments.put(enchantment.getName(), enchantment); + + if (!CompatUtil.isPre1_13()) { + enchantments.put(enchantment.getKey().getKey(), enchantment); + } + }); + // enchantments.put("power", Enchantment.ARROW_DAMAGE); + // enchantments.put("flame", Enchantment.ARROW_FIRE); + // enchantments.put("infinity", Enchantment.ARROW_INFINITE); + // enchantments.put("punch", Enchantment.ARROW_KNOCKBACK); + // enchantments.put("sharpness", Enchantment.DAMAGE_ALL); + // enchantments.put("baneofarthopods", Enchantment.DAMAGE_ARTHROPODS); + // enchantments.put("smite", Enchantment.DAMAGE_UNDEAD); + // enchantments.put("efficiency", Enchantment.DIG_SPEED); + // enchantments.put("unbreaking", Enchantment.DURABILITY); + // enchantments.put("thorns", Enchantment.THORNS); + // enchantments.put("fireaspect", Enchantment.FIRE_ASPECT); + // enchantments.put("knockback", Enchantment.KNOCKBACK); + // enchantments.put("fortune", Enchantment.LOOT_BONUS_BLOCKS); + // enchantments.put("looting", Enchantment.LOOT_BONUS_MOBS); + // enchantments.put("respiration", Enchantment.OXYGEN); + // enchantments.put("blastprotection", Enchantment.PROTECTION_EXPLOSIONS); + // enchantments.put("featherfalling", Enchantment.PROTECTION_FALL); + // enchantments.put("fireprotection", Enchantment.PROTECTION_FIRE); + // enchantments.put("projectileprotection", Enchantment.PROTECTION_PROJECTILE); + // enchantments.put("protection", Enchantment.PROTECTION_ENVIRONMENTAL); + // enchantments.put("silktouch", Enchantment.SILK_TOUCH); + // enchantments.put("aquaaffinity", Enchantment.WATER_WORKER); + // enchantments.put("luck", Enchantment.LUCK); + ENCHANTMENTS = Collections.unmodifiableMap(enchantments); + + final Map effects = new HashMap<>(); + Arrays.stream(PotionEffectType.values()) + .forEach( + type -> { + if (type == null) { + return; + } + + effects.put(type.getName(), type); + }); + effects.put("speed", PotionEffectType.SPEED); + // effects.put("slowness", PotionEffectType.SLOW); + // effects.put("haste", PotionEffectType.FAST_DIGGING); + // effects.put("fatigue", PotionEffectType.SLOW_DIGGING); + // effects.put("strength", PotionEffectType.INCREASE_DAMAGE); + // effects.put("heal", PotionEffectType.HEAL); + // effects.put("harm", PotionEffectType.HARM); + // effects.put("jump", PotionEffectType.JUMP); + // effects.put("nausea", PotionEffectType.CONFUSION); + // effects.put("regeneration", PotionEffectType.REGENERATION); + // effects.put("resistance", PotionEffectType.DAMAGE_RESISTANCE); + effects.put("fireresistance", PotionEffectType.FIRE_RESISTANCE); + effects.put("waterbreathing", PotionEffectType.WATER_BREATHING); + effects.put("invisibility", PotionEffectType.INVISIBILITY); + effects.put("blindness", PotionEffectType.BLINDNESS); + effects.put("nightvision", PotionEffectType.NIGHT_VISION); + effects.put("hunger", PotionEffectType.HUNGER); + effects.put("weakness", PotionEffectType.WEAKNESS); + effects.put("poison", PotionEffectType.POISON); + effects.put("wither", PotionEffectType.WITHER); + effects.put("healthboost", PotionEffectType.HEALTH_BOOST); + effects.put("absorption", PotionEffectType.ABSORPTION); + effects.put("saturation", PotionEffectType.SATURATION); + EFFECTS = Collections.unmodifiableMap(effects); + } + + public static ItemStack loadFromString(final String line) { + if (line == null || line.isEmpty()) { + throw new IllegalArgumentException("Line is empty or null"); + } + + final String[] args = line.split(" +"); + String[] materialData = args[0].split(":"); + Material material = Material.matchMaterial(materialData[0]); + + // TEMP: Allow confirm button item loading in 1.13 + if (!CompatUtil.isPre1_13()) { + if (materialData[0].equalsIgnoreCase("STAINED_CLAY")) { + material = Material.TERRACOTTA; + + if (materialData.length > 1) { + material = Terracottas.from((short) NumberUtil.parseLong(materialData[1]).orElse(0)); + } + } + } + + if (material == null) { + throw new IllegalArgumentException("'" + args[0] + "' is not a valid material"); + } + + ItemStack result = new ItemStack(material, 1); + + if (materialData.length > 1) { + // Handle potions and spawn eggs switching to NBT in 1.9+ + if (!CompatUtil.isPre1_9()) { + if (material.name().contains("POTION")) { + final List values = Arrays.asList(materialData[1].split("-")); + final PotionType type; + + if ((type = EnumUtil.getByName(values.get(0), PotionType.class)) == null) { + throw new IllegalArgumentException( + "'" + + values.get(0) + + "' is not a valid PotionType. Available: " + + EnumUtil.getNames(PotionType.class)); + } + + final PotionMeta meta = (PotionMeta) result.getItemMeta(); + meta.setBasePotionData( + new PotionData(type, values.contains("extended"), values.contains("strong"))); + result.setItemMeta(meta); + } else if (CompatUtil.isPre1_13() && material.name().equals("MONSTER_EGG")) { + final EntityType type; + + if ((type = EnumUtil.getByName(materialData[1], EntityType.class)) == null) { + throw new IllegalArgumentException( + "'" + + materialData[0] + + "' is not a valid EntityType. Available: " + + EnumUtil.getNames(EntityType.class)); + } + + result = new SpawnEggs(type).toItemStack(); + } + } + + final OptionalLong value; + + if ((value = NumberUtil.parseLong(materialData[1])).isPresent()) { + result.setDurability((short) value.getAsLong()); + } + } + + if (args.length < 2) { + return result; + } + + result.setAmount(Integer.parseInt(args[1])); + + if (args.length > 2) { + for (int i = 2; i < args.length; i++) { + final String argument = args[i]; + final String[] pair = argument.split(":", 2); + + if (pair.length < 2) { + continue; + } + + applyMeta(result, pair[0], pair[1]); + } + } + + return result; + } + + public static ItemStack loadFromString(final String line, final Consumer errorHandler) { + ItemStack result; + + try { + result = loadFromString(line); + } catch (Exception ex) { + result = + ItemBuilder.of(Material.REDSTONE_BLOCK) + .name("&4&m------------------") + .lore( + "&cThere was an error", + "&cwhile loading this", + "&citem, please contact", + "&can administrator.", + "&4&m------------------") + .build(); + errorHandler.accept(ex.getMessage()); + } + + return result; + } + + private static void applyMeta(final ItemStack item, final String key, final String value) { + final ItemMeta meta = item.getItemMeta(); + + if (key.equalsIgnoreCase("name")) { + meta.setDisplayName(StringUtil.color(value.replace("_", " "))); + item.setItemMeta(meta); + return; + } + + if (key.equalsIgnoreCase("lore")) { + meta.setLore( + StringUtil.color(Lists.newArrayList(value.split("\\|")), line -> line.replace("_", " "))); + item.setItemMeta(meta); + return; + } + + if (key.equalsIgnoreCase("unbreakable") && value.equalsIgnoreCase("true")) { + if (CompatUtil.isPre1_12()) { + // meta.spigot().setUnbreakable(true); + } else { + meta.setUnbreakable(true); + } + + item.setItemMeta(meta); + return; + } + + if (key.equalsIgnoreCase("flags")) { + final String[] flags = value.split(","); + + for (final String flag : flags) { + final ItemFlag itemFlag = EnumUtil.getByName(flag, ItemFlag.class); + + if (itemFlag == null) { + continue; + } + + meta.addItemFlags(itemFlag); + } + + item.setItemMeta(meta); + return; + } + + final Enchantment enchantment = ENCHANTMENTS.get(key); + + if (enchantment != null) { + item.addUnsafeEnchantment(enchantment, Integer.parseInt(value)); + return; + } + + if (item.getType().name().contains("POTION")) { + final PotionEffectType effectType = EFFECTS.get(key); + + if (effectType != null) { + final String[] values = value.split(":"); + final PotionMeta potionMeta = (PotionMeta) meta; + potionMeta.addCustomEffect( + new PotionEffect(effectType, Integer.parseInt(values[1]), Integer.parseInt(values[0])), + true); + item.setItemMeta(potionMeta); + return; + } + } + + if (Items.equals(Items.HEAD, item) + && (key.equalsIgnoreCase("player") + || key.equalsIgnoreCase("owner") + || key.equalsIgnoreCase("texture"))) { + final SkullMeta skullMeta = (SkullMeta) meta; + + // Since Base64 texture strings are much longer than usernames... + if (value.length() > 16) { + Skulls.setSkull(skullMeta, value); + } else { + skullMeta.setOwner(value); + } + + item.setItemMeta(skullMeta); + } + + if (item.getType().name().contains("LEATHER_") && key.equalsIgnoreCase("color")) { + final LeatherArmorMeta leatherArmorMeta = (LeatherArmorMeta) meta; + final String[] values = value.split(","); + leatherArmorMeta.setColor( + Color.fromRGB( + Integer.parseInt(values[0]), + Integer.parseInt(values[1]), + Integer.parseInt(values[2]))); + item.setItemMeta(leatherArmorMeta); + } + + if (key.equalsIgnoreCase("custommodeldata") && !CompatUtil.isPre1_14()) { + meta.setCustomModelData(Integer.parseInt(value)); + item.setItemMeta(meta); + } + } + + public static void copyNameLore(final ItemStack from, final ItemStack to) { + final ItemMeta fromMeta = from.getItemMeta(), toMeta = to.getItemMeta(); + + if (fromMeta.hasDisplayName()) { + toMeta.setDisplayName(fromMeta.getDisplayName()); + } + + if (fromMeta.hasLore()) { + toMeta.setLore(fromMeta.getLore()); + } + + to.setItemMeta(toMeta); + } + + private ItemUtil() {} +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/profile/NameFetcher.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/profile/NameFetcher.java new file mode 100644 index 00000000..0b7e405d --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/profile/NameFetcher.java @@ -0,0 +1,129 @@ +package me.realized.tokenmanager.util.profile; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public final class NameFetcher { + + private static final ScheduledExecutorService EXECUTOR_SERVICE = + Executors.newSingleThreadScheduledExecutor(); + private static final String MOJANG_URL = + "https://sessionserver.mojang.com/session/minecraft/profile/"; + private static final String MINETOOLS_URL = "https://api.minetools.eu/uuid/"; + private static final JsonParser JSON_PARSER = new JsonParser(); + private static final Cache UUID_TO_NAME = + CacheBuilder.newBuilder() + .concurrencyLevel(4) + .maximumSize(1000) + .expireAfterWrite(30, TimeUnit.MINUTES) + .build(); + + private NameFetcher() {} + + static void getNames(final List uuids, final Consumer> consumer) { + EXECUTOR_SERVICE.schedule(new NameCollectorTask(uuids, consumer), 0L, TimeUnit.MILLISECONDS); + } + + private static String getName(final UUID uuid, final String url) { + final String cached = UUID_TO_NAME.getIfPresent(uuid); + + if (cached != null) { + return cached; + } + + try { + final HttpURLConnection connection = + (HttpURLConnection) new URL(url + uuid.toString().replace("-", "")).openConnection(); + connection.setRequestProperty("User-Agent", "Mozilla/5.0"); + connection.connect(); + + try (final InputStream stream = connection.getInputStream()) { + if (stream.available() == 0) { + return null; + } + + final JsonObject response = (JsonObject) JSON_PARSER.parse(new InputStreamReader(stream)); + final String name = response.get("name").getAsString(); + + if (name != null) { + UUID_TO_NAME.put(uuid, name); + return name; + } + } + } catch (Exception ignored) { + } + return null; + } + + private static class NameCollectorTask implements Runnable { + + private final List keys = new ArrayList<>(); + private final Consumer> consumer; + private final Map names = new HashMap<>(); + + NameCollectorTask(final List uuids, final Consumer> consumer) { + uuids.forEach(uuid -> keys.add(new Key(uuid))); + this.consumer = consumer; + } + + @Override + public void run() { + if (keys.isEmpty()) { + consumer.accept(names); + return; + } + + final Key first = keys.get(0); + + if (first.attempts > 1) { + keys.remove(first); + } else { + final String name; + final Player player; + + if ((player = Bukkit.getPlayer(first.uuid)) != null) { + name = player.getName(); + } else { + name = getName(first.uuid, first.attempts == 0 ? MINETOOLS_URL : MOJANG_URL); + } + + first.attempts++; + + if (name != null) { + names.put(first.uuid, name); + keys.remove(first); + } + } + + // Run with delay to not trigger the rate limit + EXECUTOR_SERVICE.schedule(this, 250L, TimeUnit.MILLISECONDS); + } + + private class Key { + + private final UUID uuid; + private int attempts; + + Key(final UUID uuid) { + this.uuid = uuid; + } + } + } +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/profile/ProfileUtil.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/profile/ProfileUtil.java new file mode 100644 index 00000000..b5d176f9 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/profile/ProfileUtil.java @@ -0,0 +1,55 @@ +package me.realized.tokenmanager.util.profile; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.regex.Pattern; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public final class ProfileUtil { + + private static final Pattern UUID_PATTERN = + Pattern.compile( + "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"); + + public static boolean isOnlineMode() { + if (Bukkit.getOnlineMode()) { + return true; + } + + try { + Class clazz = Class.forName("org.spigotmc.SpigotConfig"); + return (boolean) clazz.getField("bungee").get(null); + } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException ex) { + return false; + } + } + + public static boolean isUUID(final String s) { + return UUID_PATTERN.matcher(s).matches(); + } + + public static void getNames(final List uuids, final Consumer> consumer) { + NameFetcher.getNames(uuids, consumer); + } + + public static void getUUID( + final String name, final Consumer consumer, final Consumer errorHandler) { + final Player player; + + if ((player = Bukkit.getPlayerExact(name)) != null) { + consumer.accept(player.getUniqueId().toString()); + return; + } + + try { + consumer.accept(UUIDFetcher.getUUID(name)); + } catch (Exception ex) { + errorHandler.accept(ex.getMessage()); + } + } + + private ProfileUtil() {} +} diff --git a/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/profile/UUIDFetcher.java b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/profile/UUIDFetcher.java new file mode 100644 index 00000000..0a33d401 --- /dev/null +++ b/plugins/TokenManager/src/main/java/me/realized/tokenmanager/util/profile/UUIDFetcher.java @@ -0,0 +1,84 @@ +package me.realized.tokenmanager.util.profile; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Collections; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** Modified version of UUIDFetcher by evilmidget38 */ +final class UUIDFetcher { + + private static final String PROFILE_URL = "https://api.mojang.com/profiles/minecraft"; + private static final JsonParser JSON_PARSER = new JsonParser(); + private static final Gson GSON = new Gson(); + private static final Cache NAME_TO_UUID = + CacheBuilder.newBuilder() + .concurrencyLevel(4) + .maximumSize(1000) + .expireAfterWrite(30, TimeUnit.MINUTES) + .build(); + + private UUIDFetcher() {} + + static String getUUID(final String name) throws Exception { + final UUID cached = NAME_TO_UUID.getIfPresent(name); + + if (cached != null) { + return cached.toString(); + } + + final HttpURLConnection connection = createConnection(); + final String body = GSON.toJson(Collections.singletonList(name)); + writeBody(connection, body); + + try (Reader reader = new InputStreamReader(connection.getInputStream())) { + JsonArray array = (JsonArray) JSON_PARSER.parse(reader); + final JsonObject profile = (JsonObject) array.get(0); + final UUID uuid; + NAME_TO_UUID.put( + profile.get("name").getAsString(), uuid = get(profile.get("id").getAsString())); + return uuid.toString(); + } + } + + private static void writeBody(HttpURLConnection connection, String body) throws Exception { + OutputStream stream = connection.getOutputStream(); + stream.write(body.getBytes()); + stream.flush(); + stream.close(); + } + + private static HttpURLConnection createConnection() throws Exception { + URL url = new URL(PROFILE_URL); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setUseCaches(false); + connection.setDoInput(true); + connection.setDoOutput(true); + return connection; + } + + private static UUID get(String id) { + return UUID.fromString( + id.substring(0, 8) + + "-" + + id.substring(8, 12) + + "-" + + id.substring(12, 16) + + "-" + + id.substring(16, 20) + + "-" + + id.substring(20, 32)); + } +} diff --git a/plugins/TokenManager/src/main/resources/config.yml b/plugins/TokenManager/src/main/resources/config.yml new file mode 100644 index 00000000..25afe6ae --- /dev/null +++ b/plugins/TokenManager/src/main/resources/config.yml @@ -0,0 +1,81 @@ +# DO NOT EDIT THIS VALUE! +config-version: 9 + +# If enabled, a notification and the download link will be printed on console whenever an update is available. +check-for-updates: true + +# Allows you to force set the plugin's mode to online/offline/automatic. +# If you run a cracked server but the plugin detects it as online and saves UUID instead of name, set the value below to 'false'. +# Available modes: true, false, auto (default) +online-mode: auto + +# Prevents players from sending token to a player who has the same ip address as the sender. +alt-prevention: false + +# The default token balance of a first-time joiner. +default-balance: 25 + +# Sets a min/max limit on the amount specified in /token send command. To disable, set the value to -1. +send-amount-limit: + min: 1 + max: -1 + +shop: + # When the command '/token shop' is executed, enabling this option will open the specified shop below. (Even if a shop name is provided) + open-selected: + enabled: false + shop: example + + # Settings for the confirm purchase gui. + confirm-purchase-gui: + title: 'Confirm Your Purchase' + # Use the placeholder %price% in the name or the lore of the confirm-button to display the price of the item to be purchased. + # NOTE: In Minecraft 1.13, stained glass panes were assigned their own material name. If buttons below won't load, use the new material name. + confirm-button: 'STAINED_CLAY:5 1 name:&a&lBUY lore:&7Price:_&a%price%_tokens' + cancel-button: 'STAINED_CLAY:14 1 name:&c&lCANCEL' + + # Set a delay between purchases. 0 for no delay. + click-delay: 0 + + # If enabled, players with full inventory will not be able to purchase from token shops. + check-inventory-full: false + + # If enabled, all shop purchases will be logged to console. + log-purchases: false + +# [WARNING: DANGER ZONE] +data: + # If enabled, the plugin will use selected MySQL database as the token balance storage. + # Note: To transfer all data from data.yml to the MySQL database, use the command '/tm transfer'. (MySQL must be enabled below!) + mysql: + enabled: false + hostname: '127.0.0.1' + port: '3306' + username: 'root' + password: 'password' + database: 'database' + table: 'tokenmanager' + url: 'jdbc:mysql://%hostname%:%port%/%database%' + + # This section is optional. Using mysql database for a single server will work just fine even if no redis server is provided. + # If mysql is enabled, a redis server can be used to synchronize balances as they are modified. + redis: + enabled: true + server: '127.0.0.1' + port: '6379' + password: '' + + # WARNING + # + # 1. Enabling this will set TokenManager's economy as your server's main economy. + # Your old economy data will most likely RESET, and commands related to Economy + # such as Essentials' /eco won't function properly if target player is offline. + # + # 2. If you're using Essentials Economy, make sure to reset everyone's balance + # before enabling this option! Otherwise, the old Essentials balance of the players + # might act as a token balance. + # + register-economy: false + + # /token top will be updated every x minutes. Value must be higher than 0! + balance-top-update-interval: 5 \ No newline at end of file diff --git a/plugins/TokenManager/src/main/resources/lang.yml b/plugins/TokenManager/src/main/resources/lang.yml new file mode 100644 index 00000000..0c839df6 --- /dev/null +++ b/plugins/TokenManager/src/main/resources/lang.yml @@ -0,0 +1,89 @@ +# DO NOT EDIT THIS VALUE! +config-version: 8 + +# Define placeholders usable in any message below. +# Example: Adding 'PREFIX: "[Cool]"' below and then putting '{PREFIX}' in a message will display '[Cool]' when the message is sent in game. +STRINGS: + PREFIX: '&bTM &8»' + HEADER: '&8&m*]----------[*&r &9T&boken&9M&banager &8&m*]----------[*&r' + FOOTER: '&8&m*]------------------------------------[*&r' + +ERROR: + no-permission: '&cNope! You are missing %permission% to do this.' + invalid-amount: '&c''%input%'' is not a valid amount!' + player-not-found: '&cPlayer ''%input%'' is not online.' + shop-not-found: '&cShop ''%input%'' does not exist.' + invalid-sub-command: '{PREFIX} &7''&b%input%&7'' is not a valid sub command. Please type ''&b/%command%&7'' for help.' + balance-not-enough: '&cNot enough tokens! You need %needed% tokens more.' + data-not-enough: '&cNot enough data found to display the top token balances.' + on-click-cooldown: '&cYou''re clicking too fast! Try with a little delay. (%remaining%)' + target-is-self: '&cYou cannot send tokens to yourself.' + target-has-same-ip: '&cYou cannot send tokens to this player. (alt prevention)' + no-item-in-hand: '&cYou must be holding an item to do this.' + item-is-worthless: '&cNo worth has been defined for this item.' + inventory-is-full: '&cYour inventory is full!' + not-enough-space: '&cYou need at least %slots% empty inventory slots to do this.' + on-slot-cancel: '&cYou cannot purchase this item again.' + +COMMAND: + token: + usage: + - '{HEADER}' + - '&7Your token balance: &b%tokens% tokens' + - '&b/token balance &9<&busername&9> &8» &7Check people''s token balance.' + - '&b/token send &9<&busername&9> <&bamount&9> &8» &7Send tokens to players.' + - '&b/token shop &9<&bname&9> &8» &7Opens selected shop.' + - '&b/token shops &8» &7Displays a list of loaded shops.' + - '&b/token sell &8» &7Converts your held item or the entire inventory to tokens.' + - '&b/token worth &8» &7Shows the worth of your held item or the entire inventory.' + - '&b/token top &8» &7Displays the top token balances.' + - '{FOOTER}' + + send: '{PREFIX} &7Sent &b%amount% tokens &7to %player%.' + receive: '{PREFIX} &7Received &b%amount% tokens &7from %player%.' + balance: '{PREFIX} &7Your token balance is &b%tokens% token(s)&7.' + balance-other: '{PREFIX} &7%player%''s token balance is &b%tokens% token(s)&7.' + balance-top: + next-update: '{PREFIX} &7Next update: &b%remaining%' + header: '&8&m*]------[*&r &7Top &b10 &7balances &8&m*]------[*&r' + display-format: '&7%rank%. &b%name% &7with &b%tokens% token(s)' + footer: '&8&m*]------------------------------[*&r' + shops: '{PREFIX} &7Shops: &b%shops%' + sell: '{PREFIX} &7Sold &f%item_type% (&ax%item_amount%&f) &7for &b%amount% tokens&7.' + sell-all: '{PREFIX} &7Sold &f%item_amount% item(s) &7for &b%amount% tokens&7.' + worth: '{PREFIX} &f%item_type% (&ax%item_amount%&f) &7is worth &b%amount% tokens&7. Type &f/token sell &7to sell this item.' + worth-all: + item-format: '&a%item_type% (x%item_amount%): &l+%amount% tokens' + total: '{PREFIX} &7Your inventory &f(%item_amount% items) &7is worth &b%amount% tokens&7. Type &f/token sell all &7to sell your inventory.' + + tokenmanager: + usage: + - '{HEADER}' + - '&7Required permission: &ctokenmanager.admin' + - '&b/tokenmanager add &9<&busername&9> <&bamount&9> &8» &7Add tokens to player.' + - '&b/tokenmanager remove &9<&busername&9> <&bamount&9> &8» &7Remove tokens from player.' + - '&b/tokenmanager set &9<&busername&9> <&bamount&9> &8» &7Set tokens of a player.' + - '&b/tokenmanager open &9<&busername&9> &9<&bname&9> &8» &7Open a shop for player.' + - '&b/tokenmanager giveall &9<&bamount&9> &8» &7Gives amount of tokens to online players.' + - '&b/tokenmanager transfer &8» &7MySQL enabled: data.yml -> db. Flatfile enabled: db -> sqldump.yml' + - '&b/tokenmanager reload &8» &7Reloads the plugin.' + - '{FOOTER}' + + add: '{PREFIX} &7Added &b%amount% tokens &7to %player%.' + remove: '{PREFIX} &7Removed &b%amount% tokens &7from %player%.' + set: '{PREFIX} &7Set %player%''s tokens to &b%amount% tokens&7.' + giveall: '{PREFIX} &7Gave &b%amount% tokens &7to &b%players% &7players.' + open: '{PREFIX} &7Opened shop &b%name% &7for %player%.' + + sub-command-usage: '{PREFIX} &7Usage of this sub command is ''&b/%command% %usage%&7''.' + add: '{PREFIX} &b%amount% tokens &7has been added to your balance.' + remove: '{PREFIX} &b%amount% tokens &7has been removed from your balance.' + +PLACEHOLDER: + rank: + loading: 'Loading...' + no-data: 'No data' + unranked: 'Unranked' + top: + loading: 'Loading...' + no-data: 'No data' \ No newline at end of file diff --git a/plugins/TokenManager/src/main/resources/plugin.yml b/plugins/TokenManager/src/main/resources/plugin.yml new file mode 100644 index 00000000..8044ca15 --- /dev/null +++ b/plugins/TokenManager/src/main/resources/plugin.yml @@ -0,0 +1,36 @@ +name: TokenManager +main: me.realized.tokenmanager.TokenManagerPlugin +version: @VERSION@ +softdepend: [PlaceholderAPI, Vault] +api-version: 1.14 + +author: Realized +description: A simple economy plugin for your server. +website: https://www.spigotmc.org/resources/tokenmanager.8610/ + +commands: + token: + description: Main command of TokenManager. + aliases: [tokens] + tm: + description: Administrative command of TokenManager. + aliases: [tokenmanager] + +permissions: + tokenmanager.*: + description: Grants access to all TokenManager permissions. + children: + tokenmanager.use.*: true + tokenmanager.admin: true + tokenmanager.use.*: + description: Grants access to all sub commands of /token. + children: + tokenmanager.use: true + tokenmanager.use.send: true + tokenmanager.use.top: true + tokenmanager.use.shop: true + tokenmanager.use.balance.others: true + tokenmanager.use.sell: true + tokenmanager.use.sell.all: true + tokenmanager.use.worth: true + tokenmanager.use.worth.all: true diff --git a/plugins/TokenManager/src/main/resources/shops.yml b/plugins/TokenManager/src/main/resources/shops.yml new file mode 100644 index 00000000..ba250c23 --- /dev/null +++ b/plugins/TokenManager/src/main/resources/shops.yml @@ -0,0 +1,120 @@ +#----------------------------------------------------------------------------------------- +# +# Helpful Information: +# +# - Item names: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html +# +# +# - Enchantment names: http://pastebin.com/0WktGa59 +# +# - (Example of adding an enchantment) +# - displayed: 276 1 sharpness:3 smite:2 unbreaking:3 +# +# +# - PotionEffect names: http://pastebin.com/eghUaTuJ +# +# - (Example of adding a custom potion effect to your displayed potion item) +# - displayed: 373:8258 1 strength:2:200 speed:3:600 +# [In this case, the item displayed will have Strength II (0:10) and Speed III (0:30).] +# +# +# [IMPORTANT] +# In Minecraft 1.9, potions & mob spawn eggs no longer use durability to specify the type. +# +# NOTE: In Minecraft 1.13, Spawn Eggs were given their own material name! +# +# (1.9+ - 1.12.2) - Example of setting mob spawn egg (zombie) as a displayed item +# +# displayed: MONSTER_EGG:ZOMBIE 1 name:&9Purchase_a_Zombie_Spawn_Egg! +# +# +# (1.9+) - Example of setting potion (splash strength II) as a displayed item +# +# displayed: POTION:STRENGTH-strong-splash 1 name:&cSplash_Potion_of_Strength_II +# +# +# (1.9+) - Example of setting potion (speed I extended) as a displayed item +# +# displayed: POTION:SPEED-extended 1 name:&bPotion_of_Speed_(Extended) +# +# +# - 1.9+ Potions: http://minecraft.gamepedia.com/Potion#Data_values +# +#----------------------------------------------------------------------------------------- + +shops: + example: # Name of your shop + title: '&6Example Shop Title' # Title of the inventory gui displayed in-game + rows: 1 # Rows of your shop inventory, must be a number between 1 - 6 + auto-close: true # 'true' to close the shop inventory when a player purchases an item + items: + '0': # Slot of the item, 0 is the first slot of the inventory gui + displayed: DIAMOND_SWORD 1 sharpness:5 fireaspect:2 name:&c&l%player%'s_Sword lore:&bExample_Line_1|&eExample_Line_2|&dBalance:_%tokens%_Tokens|&aPrice:&a_%price%_Tokens + cost: 10 # Cost of this item. %price% placeholder, usable in displayed, message, and commands, displays this value. + message: '&bTM &8» &7Thanks for your purchase, %player%! &c-%price% tokens' # Message to be sent on purchase, %player% for purchaser's name + commands: # Commands to execute on purchase, %player% for purchaser's name + - 'give %player% DIAMOND_SWORD 1 sharpness:5 fireaspect:2 name:&c&l%player%''s_Sword lore:&bExample_Line_1|&eExample_Line_2|&aPrice:_%price%_Tokens' + '1': + displayed: STONE 32 name:&3Stone_(x32) lore:&dBalance:_%tokens%_Tokens|&aPrice:_%price%_Tokens + cost: 20 + message: '&bTM &8» &7Thanks for your purchase, %player%! &c-%price% tokens' + commands: + - 'give %player% STONE 32' + '2': + displayed: DIAMOND 4 name:&bDiamond_(x4) lore:&dBalance:_%tokens%_Tokens|&aPrice:_%price%_Tokens|&c&lREQUIRES_PERMISSION! + cost: 50 + use-permission: true # 'true' to require players to have the permission 'tokenmanager.use.example-2' to purchase this item + message: '&bTM &8» &7Thanks for your purchase, %player%! &c-%price% tokens' + commands: + - 'give %player% DIAMOND 4' + '3': + displayed: WATER_BUCKET 1 name:&9Water_Bucket lore:&dBalance:_%tokens%_Tokens|&aPrice:_%price%_Tokens + cost: 15 + confirm-purchase: true # 'true' to open a confirm purchase gui to make sure player intended to purchase this item + message: '&bTM &8» &7Thanks for your purchase, %player%! &c-%price% tokens' + commands: + - 'give %player% WATER_BUCKET 1' + '4': + displayed: LAVA_BUCKET 2 name:&cLava_Bucket_(x2) lore:&dBalance:_%tokens%_Tokens|&aPrice:_%price%_Tokens + cost: 30 + empty-slots-required: 2 # Amount of empty inventory slots required for this item. If not specified, it defaults to 0. + message: '&bTM &8» &7Thanks for your purchase, %player%! &c-%price% tokens' + commands: + - 'give %player% LAVA_BUCKET 2' + '5': + displayed: CHEST 1 custommodeldata:1 name:&6examplesubshop lore:&7Opens_&6examplesubshop&7. + cost: 0 + subshop: examplesubshop # Name of a subshop to open when this item is clicked on + '6': + displayed: CHEST 1 name:&dexamplesubshop2 lore:&7Opens_&dexamplesubshop2&7. + cost: 0 + subshop: examplesubshop2 + examplesubshop: + title: '&6Example Subshop Title' + rows: 2 + items: + '0': + # This will not display properly if your server version is 1.9+! Use 'POTION:INSTANT_HEAL-strong-splash' instead of '373:16421' + displayed: 373:16421 1 name:&dSplash_Potion_of_Healing lore:&dBalance:_%tokens%_Tokens|&aPrice:_%price%_Tokens + cost: 5 + commands: + - 'give %player% 373:16421 1 name:&dSplash_Potion_of_Healing' + '17': + displayed: REDSTONE_BLOCK 1 name:&cGo_Back + cost: 0 + subshop: example + examplesubshop2: + title: '&6Example Subshop 2 Title' + rows: 1 + use-permission: true # 'true' to require players to have the permission 'tokenmanager.use.examplesubshop2' to access this shop + confirm-purchase: true # 'true' to open a confirm purchase gui for every item in this shop inventory with a cost over 0 + items: + '0': + displayed: BEACON 1 name:&5Beacon lore:&dBalance:_%tokens%_Tokens|&aPrice:_%price%_Tokens + cost: 1000 + commands: + - 'give %player% BEACON 1' + '8': + displayed: REDSTONE_BLOCK 1 name:&cGo_Back + cost: 0 + subshop: example \ No newline at end of file diff --git a/plugins/TokenManager/src/main/resources/worth.yml b/plugins/TokenManager/src/main/resources/worth.yml new file mode 100644 index 00000000..7069cdd9 --- /dev/null +++ b/plugins/TokenManager/src/main/resources/worth.yml @@ -0,0 +1,3 @@ +IRON_INGOT: 5 +GOLD_INGOT: 10 +DIAMOND: 25 \ No newline at end of file