diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..7db4ee0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,28 @@ +--- +name: Bug report +about: If you think you encountered something unexpected +title: '' +labels: bug +assignees: alexcrea + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Technical informations** +Server version & framework. (/version) +Plugin version if applicable (/version CustomAnvil) +List of plugin that may interact with CustomAnvil if applicable + +**Errors** +If any errors was displayed on the console. please provide it. + +**Configuration** +Provide relavent configuration files if no error found or if you think the error is related to your configuration. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..1f9c4e1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: Feature request +about: Suggest an idea for Custom Anvil +title: '' +labels: enhancement +assignees: alexcrea + +--- + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Idea how to integrate it** +How and where do you think the config of your feature should like +How and where do you think the config gui for your feature should look like + +**Additional context** +Add any other context about the feature request here. diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..a50cacd --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,163 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + push: + branches: [ "v1.x.x", "v2.x.x" ] + pull_request: + branches: [ "v1.x.x", "v2.x.x" ] + release: + types: [published] + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + + env: + MODRINTH_VERSIONS: '["1.18.x", "1.19.x", "1.20.x", "1.21.x", "26.1.x", "26.2.x"]' + MODRINTH_PLATFORMS: '["spigot", "paper", "purpur", "folia"]' + + steps: + - uses: actions/checkout@v4 + - name: Set up JDKs + uses: actions/setup-java@v4 + with: + java-version: | + 21 + distribution: 'temurin' + cache: gradle + + - name: Cache Gradle root and wrapper + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-root-${{ runner.os }}-${{ hashFiles('build.gradle*') }} + restore-keys: gradle-root-${{ runner.os }}- + + # Setup paperweight cache + - name: Cache paperweight + uses: actions/cache@v3 + with: + path: | + ./nms/*/.gradle/caches/paperweight + key: paperweight-submodules-${{ runner.os }}-${{ hashFiles('nms/*/build.gradle*') }} + restore-keys: | + paperweight-submodules-${{ runner.os }}- + + # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies. + # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + + - name: Make gradlew executable + run: chmod +x ./gradlew + + - name: Get small commit hash + if: ${{ github.event_name != 'release' && success() }} + run: echo "SMALL_COMMIT_HASH=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV + + - name: Build with Gradle Wrapper + run: ./gradlew build --parallel --stacktrace + + # only submit dependency on push + - name: Generate and submit dependency graph + uses: gradle/actions/dependency-submission@v4 + if: ${{ github.event_name == 'push' && success() }} + continue-on-error: true + + # Get the names of the online and offline jars + # grep -v "offline" to exclude offline jar as the regex would catch it otherwise + - name: Get file name for jars + run: | + ONLINE_JAR_PATH=$(ls build/libs/CustomAnvil-*.jar | grep -v "offline") + OFFLINE_JAR_PATH=$(ls build/libs/CustomAnvil-*-offline.jar) + + echo "ONLINE_JAR_NAME=$(basename $ONLINE_JAR_PATH)" >> $GITHUB_ENV + echo "OFFLINE_JAR_NAME=$(basename $OFFLINE_JAR_PATH)" >> $GITHUB_ENV + + # upload the named jars as artifact + - name: Upload online JAR artifact + uses: actions/upload-artifact@v4 + with: + name: CustomAnvil.jar + path: build/libs/${{ env.ONLINE_JAR_NAME }} + + - name: Upload offline JAR file + uses: actions/upload-artifact@v4 + with: + name: CustomAnvil-offline.jar + path: build/libs/${{ env.OFFLINE_JAR_NAME }} + + - name: Summarize tests results + uses: jeantessier/test-summary-action@v1 + if: ${{ always() }} + + # upload the jar to release + - name: Upload jar to release + if: ${{ github.event_name == 'release' && success() }} + uses: softprops/action-gh-release@v2 + with: + files: | + build/libs/${{ env.ONLINE_JAR_NAME }} + build/libs/${{ env.OFFLINE_JAR_NAME }} + + - name: Hangar release + if: ${{ (github.event_name != 'release' || github.event_name != 'push') && github.repository_owner == 'alexcrea' && success() }} + env: + HANGAR_API_TOKEN: ${{ secrets.HANGAR_API_TOKEN }} + RELEASE_CHANGELOG: ${{ github.event.release.body }} + run: ./gradlew publishAllPublicationsToHangar --stacktrace + + - name: Modrinth publish alpha + if: ${{ github.event_name == 'push' && github.repository_owner == 'alexcrea' && success() }} + uses: cloudnode-pro/modrinth-publish@v2 + with: + token: ${{ secrets.MODRINTH_TOKEN }} + project: S75Ueiq9 + name: dev-${{ env.SMALL_COMMIT_HASH }} + version: dev-${{ env.SMALL_COMMIT_HASH }} + loaders: ${{ env.MODRINTH_PLATFORMS }} + game-versions: ${{ env.MODRINTH_VERSIONS }} + channel: alpha + files: build/libs/${{ env.ONLINE_JAR_NAME }} + changelog: ${{ github.event.head_commit.message }} + + - name: Modrinth publish release + if: ${{ github.event_name == 'release' && github.repository_owner == 'alexcrea' && success() }} + uses: cloudnode-pro/modrinth-publish@v2 + with: + token: ${{ secrets.MODRINTH_TOKEN }} + project: S75Ueiq9 + name: ${{ github.event.release.name }} + version: ${{ github.event.release.tag_name }}${{ github.event.release.prerelease && '-pre' || '' }} + loaders: ${{ env.MODRINTH_PLATFORMS }} + game-versions: ${{ env.MODRINTH_VERSIONS }} + channel: ${{ github.event.release.prerelease == false && 'release' || 'beta' }} + files: build/libs/${{ env.ONLINE_JAR_NAME }} + changelog: ${{ github.event.release.body }} + + - name: Send release note to discord + if: ${{ github.event_name == 'release' && github.repository_owner == 'alexcrea' && success() }} + uses: tsickert/discord-webhook@v7.0.0 + with: + webhook-url: ${{ secrets.RELEASE_WEBHOOK_URL }} + content: | + ${{ github.event.release.prerelease == false && '<@&1338546156325568642>' || '<@&1352296092989001768>' }} + # New ${{ github.event.release.prerelease && 'beta ' || '' }}version of custom anvil ! <:CustomAnvil:1262550667986342001>([Modrinth](https://modrinth.com/plugin/customanvil), [Hangar](https://hangar.papermc.io/alexcrea/CustomAnvil) and [GitHub](${{ github.event.release.html_url }}) links) + -# note: automated release. spigot is not uploaded yet. + + ${{ github.event.release.body }} diff --git a/.gitignore b/.gitignore index 80d6030..982299c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,21 @@ /.gradle/ /build/ /out/ +.lastDeploymentsId + +#nms & impl submodule build directory ignored +/nms/build +/nms/.gradle +/nms/*/build +/nms/*/.gradle +/impl/build +/impl/.gradle +/impl/*/build +/impl/*/.gradle + +# run folder +/run/ + +# other random folders +/htmlReport +/.kotlin/errors diff --git a/.idea/icon.png b/.idea/icon.png new file mode 100644 index 0000000..06945f2 Binary files /dev/null and b/.idea/icon.png differ diff --git a/.run/Server.run.xml b/.run/Server.run.xml new file mode 100644 index 0000000..9a8887f --- /dev/null +++ b/.run/Server.run.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md new file mode 100644 index 0000000..9d37521 --- /dev/null +++ b/COMPATIBILITY.md @@ -0,0 +1,63 @@ +### Bedrock issue +For server using geyser, bedrock player cannot use custom "recipes" in the anvil. +This is cannot be fixed on geyser or my side. + +### Plugin Compatibility +Here is various plugins that had issues with CustomAnvil +where efforts was made for compatibility and should be working right: + +some if not all of them are cool ! I recommend checking them out ! + +## Supported By CustomAnvil +These plugins have compatibility handled by custom anvil. seek help on custom anvil and do not bother these developers + +#### Enchantment Plugins +- [ExcellentEnchants](https://www.spigotmc.org/resources/excellentenchants-%E2%AD%90-75-vanilla-like-enchantments.61693/) by NightExpress: + Use ExcellentEnchants item type \ + Also use ExcellentEnchant max enchant limit + +- [EcoEnchant](https://www.spigotmc.org/resources/ecoenchants-%E2%AD%95-250-enchantments-%E2%9C%85-create-custom-enchants-%E2%9C%A8-essentials-cmi-support.79573/) by Auxilor: + Need to use /anvilconfigreload or a server restart to add newly added enchantment. + Use EcoEnchant restriction system but new restriction can be added in custom anvil + +- [Enchantment²](https://www.spigotmc.org/resources/enchants-squared-the-enchantsplus-rewrite-custom-enchantments-that-act-like-vanilla-ones.86747/) by Athlaeos: + Support by Custom Anvil but still experimental. Automatic configuration. Plugin is not actively developed anymore + +- [SuperEnchants](https://modrinth.com/plugin/superenchants) by Aznos: + Use SuperEnchant restrictions system but new restriction can be added in custom anvil + + +#### Custom Items Plugins +Custom Items support is considered unstable. If you find issue please report it ! + +- [EcoItem](https://www.spigotmc.org/resources/30-sale%E2%8F%B3-ecoitems-%E2%AD%95-create-custom-items-%E2%9C%85-weapons-armors-tools-charms-%E2%9C%A8-item-levels-rarities.94601/) by Exanthiax: + May have some issue. but should partially work I hope + +- [ItemAdder](https://www.spigotmc.org/resources/%E2%9C%A8itemsadder%E2%AD%90emotes-mobs-items-armors-hud-gui-emojis-blocks-wings-hats-liquids.73355/) by LoneDev: + Need to fix unit item not working completly correctly as in can't have twice same base item + +#### Anvil Mechanics Plugins +- [Disenchantment](https://www.spigotmc.org/resources/disenchantment-1-21-1-1-20-6-new-book-splitting-mechanics.110741/) by H7KZ + Partially use Custom Anvil maximum XP settings (>= 6.1.5) + +- [HavenBags](https://www.spigotmc.org/resources/havenbags-shulker-like-player-bound-bags-1-17-1-21-4.110420/) by hyperdefined + For bag upgrade and skin via anvil. (version >= 1.31.0) + +- [AxPlayerWarp](https://modrinth.com/project/QDJHDKvi) by ArtillexStudios + For its anvil inventory usage + +- [ToolsStats](https://modrinth.com/project/oBZj9E15) by Valorless + For token application using anvil + +### Known Partially Incompatible +- [UberEnchant](https://modrinth.com/plugin/uberenchant) by coltonj96 + Anvil handling as they are doing something similar to CustomAnvil. +It is by no mean there faults and I recommend checking them out especially if custom anvil do not work for your use case ! + +- [AdvencedEnchantments](https://ae.advancedplugins.net/) by Advanced Plugins + Paid plugin I do not own as I did not get commissioned for support. + may be able to use api but cannot test on my side + +If you like Custom Anvil to support a specific plugin (custom enchant or anvil mechanic). +You can ask, but please note implementing compatibility will be considered +as low priority as I work for the plugin as an hobby on my free time for free. diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 0000000..56409f5 --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,34 @@ +Thanks **DelilahEve** for making [Unsafe Enchants](https://github.com/DelilahEve/UnsafeEnchants). \ +CustomAnvil was initially a fork of Unsafe Enchants where I wanted to add more and more and here we are now ! + +Thanks for all the contributors of bukkit, spigot, the paper team and the adventure API developers \ +Thanks JetBrain for making IntelliJ + +### Dependencies +These dependencies (or a modified version of) are used by custom anvil +- [IF](https://github.com/stefvanschie/IF) an inventory framework by stefvanschie +- [Mockbukkit](https://github.com/MockBukkit/MockBukkit) for unit testing +- [CentralPortalPlus](https://github.com/lalakii/central-portal-plus) by lalakii +- [test-summary-action](https://github.com/jeantessier/test-summary-action) by jeantessier +- [modrinth-publish](https://github.com/cloudnode-pro/modrinth-publish) by Zefir +- [discord-webhook](https://github.com/tsickert/discord-webhook) by tsickert +- Thanks [bstats](https://bstats.org/) for keeping me motivated +- And [FastStats](https://faststats.dev/) alternative to bstats in beta test +- [ModrinthUpdateChecker](https://github.com/Clickism/ModrinthUpdateChecker) by Clickism and thanks to the modrinth team + +### Compatibility +Thanks to all the cool creator making the minecraft plugin's ecosystem works ! \ +See [Compatibility list](https://github.com/alexcrea/CustomAnvil/blob/v1.x.x/COMPATIBILITY.md) for details + +but especially, Big Thanks for H7KZ maker of [Disenchantment](https://github.com/H7KZ/Disenchantment) + +### Special Thanks + +Thanks for all the users trying my plugin for these niche use cases +, reporting issues and giving ideas ! + +Thanks coltonj96 for [UberEnchant](https://modrinth.com/plugin/uberenchant). +we may be incompatible with the anvil, but I do think it is a good alternative ! \ +I wish one day to work on cross compatibiltiy \ +* If custom anvil do not work well for you or your use case give it a try ! * + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index 85fcbf0..9d9e678 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,111 @@ -# UnsafeEnchants+ +# Custom Anvil -**UnsafeEnchants+** is a fully configurable plugin for bukkit, spigot, and paper minecraft servers -allowing custom enchantment limits and customising combination restrictions. - -**UnsafeEnchants+** is based on [UnsafeEnchants](https://github.com/DelilahEve/UnsafeEnchants). You can find it on -[GitHub](https://github.com/DelilahEve/UnsafeEnchants/releases/latest), -[Spigot](https://www.spigotmc.org/resources/unsafe-enchants.104708/) or -[CurseForge](https://www.curseforge.com/minecraft/bukkit-plugins/unsafe-enchants/files/all) - -**UnsafeEnchants+** add the following to [UnsafeEnchants](https://github.com/DelilahEve/UnsafeEnchants): -- Make default configuration more vanilla like -- Fix a xp bug with enchanted book -- Custom enchantment restriction configuration ---- -### Know issue: -There is non known issue, if you find one please report the issue. - ---- +**Custom Anvil** is a plugin that allows server administrators to customize every aspect of the anvil's mechanics. ### Download Locations: -the plugin can be downloaded on the -[Spigot site](https://www.spigotmc.org/resources/unsafe-enchants.114884/) -or [on GitHub](https://github.com/alexcrea/UnsafeEnchantsPlus/releases/latest) +the plugin can be downloaded on + [Modrinth](https://modrinth.com/plugin/customanvil), + [Hangar](https://hangar.papermc.io/alexcrea/CustomAnvil) + or here [on GitHub](https://github.com/alexcrea/CustomAnvil/releases/latest) +--- +**Custom Anvil** have the following features: +- Vanilla like default configuration. +- Custom enchantment level limit. +- Custom anvil recipes. +- Custom enchant restrictions (allows unsafe enchantment only for a group of item or create new restriction). +- Custom items of unit repairs (repair damaged with unit of "material", for example the repair of diamond sword by diamonds). +- Custom XP cost for every aspect of the anvil. +- Permissions to bypass level limit or enchantment restriction. +- Display XP cost instead of "too expensive" when above level 40. (see below for more information) +- Can handle some custom enchantment plugins (see below for more information) +- Gui to configure the plugin in game. +- Support use of color code, hexadecimal color and minimessage for color/decoration +- (Experimental) Folia support (gui do not work) +- (Experimental) Dialog rename (allows longer rename) +- (Experimental) Anvil with monetary cost (using vault) (require dialog rename) + +And more ! + +--- ### Permissions: +Note that for most of them you also need to enable feature and in most case enable use of permission for the specfic feature (indicated with `(toggleable)`) ```yml -ue.unsafe: Allows use of custom restriction rules -ue.bypass.fuse: Bypass every enchantment restriction check. Including custom restrictions -ue.bypass.level: Bypass max level check. Including custom max level +# Generic and bypass permissions +ca.affected: Player with this permission will be affected by the plugin +ca.bypass.fuse: Allow player to combine every enchantments to every item (no custom limit) +ca.bypass.level: Allow player to bypass every level limit (no custom limit) + +# Command permissions +ca.command.reload: Allow administrator to reload the plugin's configs +ca.command.diagnostic: Allow adminastator to diagnistic some simple problem with the plugin +ca.config.edit: Allow administrator to edit the plugin's config in game + +# ----------------------------------------------------------------------------- +# Bellow permissions also require some config change to allow usage of features +# Usage of these permission is toggleable in basic config gui or config.yml +# ----------------------------------------------------------------------------- +# Permissions related to use of color and minimessage +ca.color.code: Allow player to use color code on rename if enabled (toggleable) +ca.color.code.[thecode] (for example ca.color.code.a): Allows usage of only certain color code (toggleable) +ca.color.hex: Allow player to use hexadecimal color on rename if enabled (toggleable) +ca.rename.minimessage: Allow player to use minimessage formating on rename if enabled (toggleable) + +# Permissions related to edition of the lore +ca.lore_edit.book: Allow player to edit lore via book and quil if enabled (toggleable) +ca.lore_edit.paper: Allow player to edit lore via paper if enabled (toggleable) + +# Others +ca.rename.dialog: Allow player to use the rename dialog (toggleable) ``` -### Default Configuration: +### Commands -Default configuration can be found on following links: -- [config.yml](https://github.com/alexcrea/UnsafeEnchantsPlus/blob/master/src/main/resources/config.yml) -- [enchant_conflict.yml](https://github.com/alexcrea/UnsafeEnchantsPlus/blob/master/src/main/resources/enchant_conflict.yml) -- [item_groups.yml](https://github.com/alexcrea/UnsafeEnchantsPlus/blob/master/src/main/resources/item_groups.yml) +run `/customanvil help` to get information about available commands \ +this only show subcommands you have permission for +### Supported Plugins +See the [Compatibility list](https://github.com/alexcrea/CustomAnvil/blob/v1.x.x/COMPATIBILITY.md) + +### Overriding Too Expensive + +One of the configurations allow displaying price about 40 and removing Too Expensive. \ +By how the minecraft client work: price above 40 can only be displayed green, even if the player does not own enough experience level. +spigot version 1.18 to 1.21.11 do not need any ProtocoLib dependency. (26.1.0 or above requires it) \ +Any recent paper version also are supported for this feature. +But you should wait for update for new version containing new enchantable item or new enchantments if any of this got added. +Else it is, likely, fine to use the current version you are ussing on a new paper version + +### For custom enchantment plugin developers +For information about the API, please refer to [the Wiki](https://github.com/alexcrea/CustomAnvil/wiki) \ +(Please note that the wiki is currently incomplete) + +--- + +### Default Plugin's Configurations +see [Here](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs) + +--- +### Metric And Telemetry +Custom anvil [use bstat](https://bstats.org/plugin/bukkit/Unsafe%20Enchants%20Plus/20923) +and [faststats](https://faststats.dev/project/customanvil/minecraft-plugin) for metric and error reporting. + +You can select specific telemetry or disable all in config.yml. \ +You can also [disable bstat](https://bstats.org/getting-started) and [faststats](https://faststats.dev/info) in their /plugin folder if you like too. + +faststats is in beta testing please report me or them any error you encounter + +### Credits and Thanks +Credits and thanks can be seen [here](https://github.com/alexcrea/CustomAnvil/blob/v1.x.x/CREDITS.md) + +### Planned: +- Better Folia support (make gui work. fix some dirty handled parts) +- Get restriction on unknown enchantments (planned for V2) +- More features for custom anvil craft + +### Known issue: +Most unknown registered enchantments (by unsupported custom enchantment plugin & datapacks) will not have restriction by default. Planned but no eta. + +### Do you need help with the plugin, or have any issue or suggestion? +You can ask on the discussion page, create a [GitHub issue](https://github.com/alexcrea/CustomAnvil/issues) or join my [discord](https://discord.gg/KHUNsUfRYJ)​ diff --git a/build.gradle.kts b/build.gradle.kts index 3df289d..a5dc7c3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,45 +1,455 @@ +import cn.lalaki.pub.BaseCentralPortalPlusExtension +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import groovy.util.Node +import groovy.util.NodeList +import io.papermc.hangarpublishplugin.model.HangarPublication +import io.papermc.hangarpublishplugin.model.Platforms +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import java.io.ByteArrayOutputStream + plugins { - kotlin("jvm") version "1.6.21" + kotlin("jvm") version "2.3.0" java + id("org.jetbrains.dokka").version("1.9.20") + id("com.gradleup.shadow").version("9.3.0") + // Maven publish + `maven-publish` + signing + id("cn.lalaki.central").version("1.2.8") + // Paper + id("io.papermc.paperweight.userdev") version "2.0.0-beta.17" apply false + id("io.papermc.hangar-publish-plugin") version "0.1.2" } group = "xyz.alexcrea" -version = "1.1.5" +version = "1.17.5" + +val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null +val isPreRelease = System.getenv("IS_GITHUB_PRERELEASE") == "true" + +val effectiveVersion = "$version" + + (if (isDevBuild) "-dev-${System.getenv("SMALL_COMMIT_HASH")!!}" else "") repositories { - mavenCentral() - maven(url = "https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + // EcoEnchants + maven(url = "https://repo.auxilor.io/repository/maven-public/") + + // ExcellentEnchants + maven(url = "https://repo.nightexpressdev.com/releases") + + // ItemsAdder + maven(url = "https://maven.devs.beer/") + + // For fast stats + maven { + name = "thenextlvlReleases" + url = uri("https://repo.thenextlvl.net/releases") + } + + // For vault unlocked + maven { url = uri("https://repo.codemc.io/repository/creatorfromhell/") } } +val reobfNMS = providers.gradleProperty("subprojects.reobfnms") + .get().split(",") + dependencies { - - compileOnly(kotlin("stdlib")) - + // Spigot api compileOnly("org.spigotmc:spigot-api:1.18-R0.1-SNAPSHOT") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.0") + // fast stats + implementation("dev.faststats.metrics:bukkit:0.27.0") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + // minimessage + implementation("net.kyori:adventure-text-minimessage:4.25.0") -} + // Gui library + val inventoryFramework = "xyz.alexcrea.cuanvil.inventoryframework:IF-CustomAnvil:0.10.18.2" + implementation(inventoryFramework) + testRuntimeOnly(inventoryFramework) -tasks.getByName("test") { - useJUnitPlatform() -} + // EnchantsSquaredRewritten + compileOnly(files("libs/EnchantsSquared.jar")) -// Fat-jar builder -val fatJar = tasks.register("fatJar") { - manifest { - attributes.apply { put("Main-Class", "io.delilaheve.UnsafeEnchants") } + // EcoEnchants & item + compileOnly("com.willfp:libreforge:4.79.0:all") + compileOnly("com.willfp:eco:6.74.5") + + compileOnly("com.willfp:EcoEnchants:12.11.1") + compileOnly(project(":impl:LegacyEcoEnchant")) + + compileOnly("com.willfp:EcoItems:5.66.0") + + // ExcellentEnchants + implementation(project(":impl:ExcellentEnchant5_4")) + compileOnly("su.nightexpress.excellentenchants:Core:5.1.0") { + exclude("org.spigotmc") } - archiveFileName.set("${rootProject.name}-${archiveVersion}.jar") - exclude("META-INF/*.RSA", "META-INF/*.SF", "META-INF/*.DSA") - duplicatesStrategy = DuplicatesStrategy.WARN - from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) - with(tasks.jar.get() as CopySpec) + compileOnly(files("libs/ExcellentEnchants-4.3.3-striped.jar")) // For pre v5 excellent enchants + compileOnly(files("libs/ExcellentEnchants-4.1.0-striped.jar")) // For legacy excellent enchants + + // Disenchantment + compileOnly(files("libs/Disenchantment-6.1.5.jar")) + + // HavenBags + compileOnly(files("libs/HavenBags-1.31.0.1760.jar")) + + // ToolStats + compileOnly(files("libs/toolstats-1.9.6-stripped.jar")) + + // AxPlayerWarps + compileOnly(files("libs/AxPlayerWarps-1.10.3.jar")) + + // SuperEnchants + compileOnly(files("libs/SuperEnchants-4.6.2-all.jar")) + + // ItemsAdder API + compileOnly("dev.lone:api-itemsadder:4.0.10") + + // Vault api + compileOnly("net.milkbowl.vault:VaultUnlockedAPI:2.16") + + // Include nms + implementation(project(":nms:nms-common")) + implementation(project(":nms:nms-paper")) + for (nmsPart in reobfNMS) { + implementation(project(":nms:$nmsPart", configuration = "reobf")) + } + + // include kotlin for the offline jar + implementation(kotlin("stdlib")) + + // Test dependency + testImplementation("org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.48.0") + testRuntimeOnly("commons-lang:commons-lang:2.6") } -// Ensure fatJar and copyJar are run -tasks.getByName("build") { - dependsOn(fatJar) -} \ No newline at end of file +allprojects { + apply(plugin = "java") + apply(plugin = "kotlin") + + repositories { + mavenCentral() + + // Spigot repository + maven(url = "https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + + // Paper repository + maven("https://repo.papermc.io/repository/maven-public/") + } + + dependencies { + compileOnly(kotlin("stdlib")) + + // Test dependency + testImplementation(platform("org.junit:junit-bom:5.12.2")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + } + + tasks.getByName("test") { + useJUnitPlatform() + } + + // Configure used version of kotlin and java + java { + disableAutoTargetJvm() + toolchain.languageVersion.set(JavaLanguageVersion.of(21)) + } + + // Set target version + tasks.withType().configureEach { + sourceCompatibility = + "16" // We aim for java 16 for minecraft 1.16.5. even if it not really supported by custom anvil. + targetCompatibility = "16" + + options.encoding = "UTF-8" + } + + kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_16) + } + } + +} + + +tasks { + + fun ShadowJar.configureBaseShadow(suffix: String, libraries: Array) { + val processedSuffix = if(suffix.isEmpty()) "" else "-$suffix" + val name = "${rootProject.name}-${effectiveVersion}${processedSuffix}.jar" + archiveFileName.set(name) + + // Shadow necessary dependency + relocate("com.github.stefvanschie.inventoryframework", "xyz.alexcrea.cuanvil.inventoryframework") + relocate("dev.faststats", "xyz.alexcrea.cuanvil.faststats") + + filesMatching("plugin.yml") { + expand( + "version" to effectiveVersion + processedSuffix, + "libraries" to libraries.joinToString(transform = { "\"$it\"" }), + ) + } + + // Process resource for plugin.yml + dependsOn(processResources) + } + + // Online jar (use of libraries) + shadowJar { + configureBaseShadow("", + arrayOf( + "org.jetbrains.kotlin:kotlin-stdlib:2.3.0", + "net.kyori:adventure-text-minimessage:4.25.0", + "net.kyori:adventure-text-serializer-plain:4.25.0", + "net.kyori:adventure-text-serializer-legacy:4.25.0", + )) + + // Exclude kotlin std, annotations and adventure api + exclude("*kotlin/**") + exclude("**/annotations/**") + exclude("net/kyori/**") + } + + val offlineJar by registering(ShadowJar::class) { + configureBaseShadow("offline", emptyArray()) + + from(sourceSets.main.get().output) + configurations = listOf(project.configurations.runtimeClasspath.get()) + } + + // Make the online and offline jar on build + named("build") { + dependsOn(shadowJar, offlineJar) + } + +} + +val sourcesJar by tasks.registering(Jar::class) { + archiveClassifier.set("sources") + from(kotlin.sourceSets.main.get().kotlin) +} + +val javadocJar by tasks.registering(Jar::class, fun Jar.() { + group = JavaBasePlugin.DOCUMENTATION_GROUP + description = "Assembles Javadoc JAR" + archiveClassifier.set("javadoc") + from(tasks.named("dokkaHtml")) +}) + +signing { + useGpgCmd() + val extension = extensions + .getByName("publishing") as PublishingExtension + sign(extension.publications) +} + +// ------------------------------------ +// PUBLISHING TO SONATYPE CONFIGURATION +// ------------------------------------ + +// The path is recommended to be set to an empty directory +val localMavenRepo = uri( + project.findProperty("localMavenRepo") as String? + ?: rootProject.layout.buildDirectory.dir("local-maven-repo").get().asFile.toURI() // Convert to URI +) + +centralPortalPlus { + url = localMavenRepo + username = System.getenv("SONATYPE_USERNAME") + password = System.getenv("SONATYPE_PASSWORD") + publishingType = BaseCentralPortalPlusExtension.PublishingType.USER_MANAGED // or PublishingType.AUTOMATIC +} + +object Meta { + const val desc = "spigot plugin to control every aspect of the anvil." + const val license = "GPL-3.0" + const val githubRepo = "alexcrea/CustomAnvil" + const val release = "https://s01.oss.sonatype.org/service/local/" + const val snapshot = "https://s01.oss.sonatype.org/content/repositories/snapshots/" +} + +val disallowedDependency = HashSet() +disallowedDependency.addAll(reobfNMS) +disallowedDependency.addAll(listOf("nms-common", "nms-paper", "kotlin-stdlib")) + +publishing { + repositories { + maven { + url = localMavenRepo // Specify the same local repo path in the configuration. + } + } + + publications { + create("maven") { + groupId = project.group.toString() + artifactId = project.name + version = project.version.toString() + from(components["kotlin"]) + artifact(tasks["sourcesJar"]) + artifact(tasks["javadocJar"]) + + versionMapping { + usage("java-api") { + fromResolutionOf("runtimeClasspath") + } + usage("java-runtime") { + fromResolutionResult() + } + } + + pom { + name.set(project.name) + description.set(Meta.desc) + url.set("https://github.com/${Meta.githubRepo}") + licenses { + license { + name.set(Meta.license) + url.set("https://www.gnu.org/licenses/gpl-3.0.en.html") + } + } + developers { + developer { + id.set("alexcrea") + name.set("alexcrea") + email.set("alexcrea.of@laposte.net") + url.set("https://github.com/alexcrea") + } + } + scm { + url.set( + "https://github.com/${Meta.githubRepo}.git" + ) + connection.set( + "scm:git:git://github.com/${Meta.githubRepo}.git" + ) + developerConnection.set( + "scm:git:git://github.com/${Meta.githubRepo}.git" + ) + } + issueManagement { + url.set("https://github.com/${Meta.githubRepo}/issues") + } + + withXml { + val dependenciesNode = (asNode().get("dependencies") as NodeList)[0] as Node + + val toRemove = ArrayList() + for (child in dependenciesNode.children()) { + val artifactNode = ((child as Node).get("artifactId") as NodeList)[0] as Node + val artifactID = artifactNode.value() as String + + if(disallowedDependency.contains(artifactID)) { + toRemove.add(child) + } + } + + for (node in toRemove) { + dependenciesNode.remove(node) + } + } + } + } + } +} + +// hangar publish + +fun executeGitCommand(vararg command: String): String { + val byteOut = ByteArrayOutputStream() + exec { + commandLine = listOf("git", *command) + standardOutput = byteOut + } + return byteOut.toString(Charsets.UTF_8.name()).trim() +} + + +fun latestCommitMessage(): String { + return executeGitCommand("log", "-1", "--pretty=%B") +} + +fun changelog(isOnline: Boolean): String { + var changelog = if(isDevBuild) latestCommitMessage() + else System.getenv("RELEASE_CHANGELOG") + + if(!isOnline) { + changelog = "This is an offline version of the plugin. \\\n" + + "This mean that this plugin libraries are shaded into this plugin \\\n" + + "You likely want to use the normal version of this plugin\n\n" + changelog + } + + if(changelog == null || changelog.isEmpty()) { + changelog = "empty changelog" + } + + return changelog +} + +hangarPublish { + + fun HangarPublication.configure(isOnline: Boolean, devChannel: String, releaseChannel: String) { + var versionName = effectiveVersion + if(isPreRelease) versionName+= "-pre" + if(!isOnline) versionName+= "-offline" + + version.set(versionName) + channel.set(if (isDevBuild || isPreRelease) devChannel else releaseChannel) + + changelog.set(changelog(isOnline)) + id.set("CustomAnvil") + apiKey.set(System.getenv("HANGAR_API_TOKEN")) + + platforms { + register(Platforms.PAPER) { + // Set the JAR file to upload + var task = if(isOnline) tasks.shadowJar + else tasks.named("offlineJar") + + jar.set(task.flatMap { it.archiveFile }) + + // Set platform versions from gradle.properties file + val versions: List = (property("paperVersion") as String) + .split(",") + .map { it.trim() } + platformVersions.set(versions) + + dependencies { + hangar("ProtocolLib") { + required.set(false) + } + url("Disenchantment", "https://modrinth.com/plugin/disenchantment") { + required.set(false) + } + url("ToolStats", "https://modrinth.com/plugin/toolstats") { + required.set(false) + } + url("HavenBags", "https://www.spigotmc.org/resources/havenbags-shulker-like-player-bound-bags-1-17-1-21-4.110420/") { + required.set(false) + } + url("EcoEnchants", "https://www.spigotmc.org/resources/ecoenchants-%E2%AD%95-250-enchantments-%E2%9C%85-create-custom-enchants-%E2%9C%A8-essentials-cmi-support.79573/") { + required.set(false) + } + hangar("EnchantsSquared") { + required.set(false) + } + url("ExcellentEnchants", "https://www.spigotmc.org/resources/excellentenchants-%E2%AD%90-75-vanilla-like-enchantments.61693/") { + required.set(false) + } + } + } + } + } + + publications.register("plugin") { + configure(true, "DevSnapshot", "Release") + } + + publications.register("offline") { + configure(false, "OfflineSnapshot", "OfflineRelease") + } + +} diff --git a/defaultconfigs/1.18/README.md b/defaultconfigs/1.18/README.md new file mode 100644 index 0000000..d390f3f --- /dev/null +++ b/defaultconfigs/1.18/README.md @@ -0,0 +1,6 @@ +### Default Plugin's Configurations For 1.18 to 1.20.6 +- [config.yml](https://github.com/alexcrea/CustomAnvil/blob/master/defaultconfigs/1.18/config.yml) +- [enchant_conflict.yml](https://github.com/alexcrea/CustomAnvil/blob/master/defaultconfigs/1.18/enchant_conflict.yml) +- [item_groups.yml](https://github.com/alexcrea/CustomAnvil/blob/master/defaultconfigs/1.18/item_groups.yml) +- [unit_repair_item.yml](https://github.com/alexcrea/CustomAnvil/blob/master/defaultconfigs/1.18/unit_repair_item.yml) +- [custom_recipes.yml](https://github.com/alexcrea/CustomAnvil/blob/master/defaultconfigs/1.18/custom_recipes.yml) diff --git a/defaultconfigs/1.18/config.yml b/defaultconfigs/1.18/config.yml new file mode 100644 index 0000000..b5ad3b0 --- /dev/null +++ b/defaultconfigs/1.18/config.yml @@ -0,0 +1,466 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# What service of metric should custom anvil use +# Custom anvil collect generic information like server minecraft version, type, etc... +# It can also collect error information if error is happening (currently faststats only) +# It can also be disabled +# Please refer to README for public metric link +# Possible options: auto, bstat, faststats, disabled (auto by default) +metric_type: auto + +# Allow to report errors made caused by this plugin (only for faststats) +# This allows me to fix potentials issue that I'm not aware of +# Accept true or false (true by default) +metric_collect_errors: true + +# All anvil cost will be capped to limit_repair_value if enabled. +# +# In other words: +# For any anvil cost greater than limit_repair_value, Cost will be set to limit_repair_value. +limit_repair_cost: false + +# Max cost value the Anvil can get to. +# +# Valid values include 0 to 1000. +# Cost will be displayed as "Too Expensive": +# - If Cost is above 39 +# - And replace_too_expensive is disabled (false) +limit_repair_value: 39 + +# Whether the anvil's cost limit should be removed entirely. +# +# The anvil will still visually display "Too Expensive" if "replace_too_expensive" is disabled +# However, the action will be completable if xp requirement is meet. +remove_repair_limit: false + +# Whenever anvil cost is above 39 should display the true price and not "Too Expensive". +# +# However, when bypassing "Too Expensive", anvil price will be displayed as Green. +# If the action is not completable, the cost will still be displayed as "Too expensive". +# That mean you also need to change other settings like remove_repair_limit or limit_repair_cost. +# +# Require ProtocoLib. +replace_too_expensive: false + +# XP Level amount added to the anvil when the item is repaired by another item of the same type +# +# Valid values include 0 to 1000 +item_repair_cost: 2 + +# XP Level amount added to the anvil when the item is renamed +# +# Valid values include 0 to 1000 +item_rename_cost: 1 + +# XP Level amount added to the anvil when the item is repaired by an "unit" +# For example: a Diamond on a Diamond Sword +# What's considered unit for what can be edited on the unit repair configuration. +# +# Valid values include 0 to 1000 +unit_repair_cost: 1 + +# XP Level amount added to the anvil when a sacrifice enchantment +# conflict with one of the left item enchantment +# +# Valid values include 0 to 1000 +sacrifice_illegal_enchant_cost: 1 + +# Allow using color code and hexadecimal color. +# +# Color code are prefixed by "&" and hexadecimal color by "#". +# Color code will not be applied if it colors nothing. "&&" can be used to write "&". +# For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ +allow_color_code: false +allow_hexadecimal_color: false +allow_minimessage: false + +# This enables restricting color code for player having specific permission +# It requires allow_color_code enabled for... obvious reasons +# +# For example: if player want to use "&aHello" it will be required that the player has +# the permission "ca.color.code.a" as he used the color code "a" +# In general permission to give to the player is "ca.color.code.[code]" +# where [code] is the color code you wish to allow the player +# +# It is kinda of useless when minimessage is supported as players would be able to bypass +# that using the equivalent minimessage tag +per_color_code_permission: false + +# Toggle if color should only be applicable if the player a certain permission. +# +# permission are "ca.color.code" for use of color code and "ca.color.hex" for use of hexadecimal color. +permission_needed_for_color: true + +# Xp cost if the player use color in the items name on rename. +# +# Valid values include 0 to 1000. +use_of_color_cost: 0 + +# Dialogue rename menu make use of dialog menu to allow bigger rename +# You can also change the maximum size and set it to -1 or less for maximum +# +# This feature only work on paper 1.21.7 or later +# +# At the moment only english is available for this menu... sorry ! +# +# CustomAnvil use "ca.rename.dialog" when permission +enable_dialog_rename: false +dialog_rename_max_size: 256 +permission_needed_for_dialog_rename: false + +# This allows custom anvil to not "guess" the text used for rename but store it in the item +# It will make item stackable only and only if it had used the same rename text +# +# For practical reason. this only work when dialog rename is enabled +dialog_rename_keep_user_text: true + +# Override limits for specific enchants +# +# Enchantments not listed here will use the value of default_limit +# +# Overrides provided default from aqua_affinity to depth_strider won't change effect with extra levels +# +# Valid range of 0 - 255 for each enchantment +# -1 mean keep default +enchant_limits: + minecraft:aqua_affinity: 1 + minecraft:binding_curse: 1 + minecraft:channeling: 1 + minecraft:flame: 1 + minecraft:infinity: 1 + minecraft:mending: 1 + minecraft:multishot: 1 + minecraft:silk_touch: 1 + minecraft:vanishing_curse: 1 + minecraft:depth_strider: 3 # anything more than 3 is treated as 3 by the game + minecraft:protection: 4 + minecraft:fire_protection: 4 + minecraft:blast_protection: 4 + minecraft:projectile_protection: 4 + minecraft:feather_falling: 4 + minecraft:thorns: 3 + minecraft:respiration: 3 + minecraft:sharpness: 5 + minecraft:smite: 5 + minecraft:bane_of_arthropods: 5 + minecraft:knockback: 2 + minecraft:fire_aspect: 2 + minecraft:looting: 3 + minecraft:sweeping: 3 + minecraft:sweeping_edge: 3 + minecraft:efficiency: 5 + minecraft:unbreaking: 3 + minecraft:fortune: 3 + minecraft:power: 5 + minecraft:punch: 2 + minecraft:luck_of_the_sea: 3 + minecraft:lure: 3 + minecraft:frost_walker: 2 + minecraft:impaling: 5 + minecraft:riptide: 3 + minecraft:loyalty: 3 + minecraft:piercing: 4 + minecraft:quick_charge: 3 + minecraft:soul_speed: 3 + minecraft:swift_sneak: 3 + +# Multipliers used to calculate the enchantment's value in repair/combining +# +# Values here are pulled from the fandom wiki: +# https://minecraft.fandom.com/wiki/Anvil_mechanics#Costs_for_combining_enchantments +# +# If an enchantment is missing values here, or is less than 0, it will default to 0 +# +# Calculated as: [Enchantment lvl] * [multiplier] +# +# With default values protection 4 would have a value of 4 when +# coming from either a book (4 * 1) or an item (4 * 1) +enchant_values: + minecraft:aqua_affinity: + item: 4 + book: 2 + minecraft:bane_of_arthropods: + item: 2 + book: 1 + minecraft:binding_curse: + item: 8 + book: 4 + minecraft:blast_protection: + item: 4 + book: 2 + minecraft:channeling: + item: 8 + book: 4 + minecraft:depth_strider: + item: 4 + book: 2 + minecraft:efficiency: + item: 1 + book: 1 + minecraft:flame: + item: 4 + book: 2 + minecraft:feather_falling: + item: 2 + book: 1 + minecraft:fire_aspect: + item: 4 + book: 2 + minecraft:fire_protection: + item: 2 + book: 1 + minecraft:fortune: + item: 4 + book: 2 + minecraft:frost_walker: + item: 4 + book: 2 + minecraft:impaling: + item: 4 + book: 2 + minecraft:infinity: + item: 8 + book: 4 + minecraft:knockback: + item: 2 + book: 1 + minecraft:looting: + item: 4 + book: 2 + minecraft:loyalty: + item: 1 + book: 1 + minecraft:luck_of_the_sea: + item: 4 + book: 2 + minecraft:lure: + item: 4 + book: 2 + minecraft:mending: + item: 4 + book: 2 + minecraft:multishot: + item: 4 + book: 2 + minecraft:piercing: + item: 1 + book: 1 + minecraft:power: + item: 1 + book: 1 + minecraft:projectile_protection: + item: 2 + book: 1 + minecraft:protection: + item: 1 + book: 1 + minecraft:punch: + item: 4 + book: 2 + minecraft:quick_charge: + item: 2 + book: 1 + minecraft:respiration: + item: 4 + book: 2 + minecraft:riptide: + item: 4 + book: 2 + minecraft:silk_touch: + item: 8 + book: 4 + minecraft:sharpness: + item: 1 + book: 1 + minecraft:smite: + item: 2 + book: 1 + minecraft:soul_speed: + item: 8 + book: 4 + minecraft:swift_sneak: + item: 8 + book: 4 + minecraft:sweeping: + item: 4 + book: 2 + minecraft:sweeping_edge: + item: 4 + book: 2 + minecraft:thorns: + item: 8 + book: 4 + minecraft:unbreaking: + item: 2 + book: 1 + minecraft:vanishing_curse: + item: 8 + book: 4 + +# Disable enchantment merging for level above the set value +# Enchantment merging is when, for example, 2 unbreaking II book combine to give sharpness III +# But Enchantment above this value can still be applied. following the previous example, we could still apply a unbreaking III book to a sword +# Even if disable-merge-over of unbreaking is set to 2 +# -1 mean enchantment merge for this enchantment is not disabled. default to -1 if absent. +disable-merge-over: + # Sharpness is set to -1. it equivalent to it not being set to anything (and work as vanilla on default configuration) + minecraft:sharpness: -1 + # If uncommented. 2 unbreaking II book would not give an unbreaking III book. but unbreaking III book can still be applied + #minecraft:unbreaking: 2 + +# The maximum number of enchantment an item can get. -1 for infinity +# Use eco enchant enchant_limit if present by default unless "default" is not equal to -1 +enchantment_count_limit: + default: -1 + # Limit for specific items. example bellow is an example with stick + # Per item enchantment limit override eco enchant enchant_limit and default limit + items: + stick: -1 + +# Settings for lore modification +lore_edit: + book_and_quil: + # Permission is ca.lore_edit.book + use_permission: true + append: + # If adding lore using book & quil is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Cost used for every lore line added + per_line_cost: 0 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If adding the lore consume the book & quil + do_consume: false + # Allow using color code and hexadecimal color when editing lore via book & quil + # + # Color code are prefixed by "&" and hexadecimal color by "#" + # Color code will not be applied if it colors nothing. "&&" can be used to write "&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + # + # Note that currently minimessage would disable hex code when adding color + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + + remove: + # If removing lore using book & quil is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Cost used for every lore line removed + per_line_cost: 0 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If removing the lore consume the book & quil + do_consume: false + # Cost of replacing colors + remove_color_cost: 0 + # Allowed some color and tags to be reverted to plain text + # Custom anvil will prioritise format that result is a smaller resulting text + # Note that not allowing certain format will lead to some lost of color or tags. + # If configuration are exact as append appending this book should result in the exact same color + # + # Color code will be prefixed by "&" and hexadecimal color by "#". + # If color code is allowed, "&" in the text will get converted to "&&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + + paper: + # Permission is ca.lore_edit.paper + use_permission: true + # what order should the lines should get added/removed (start/end, if invalid or not present will be end) + order: end + + append_line: + # If adding lore line using paper is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If adding the lore line consume the paper + do_consume: false + # Allow using color code and hexadecimal color when editing lore via book & quil + # + # Color code are prefixed by "&" and hexadecimal color by "#" + # Color code will not be applied if it colors nothing. "&&" can be used to write "&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + # + # Note that currently minimessage would disable hex code when adding color + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + color_use_cost: 0 + + remove_line: + # If removing lore line using paper is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If removing the lore line consume the paper + do_consume: false + # Cost of replacing colors + remove_color_cost: 0 + # Allowed some color and tags to be reverted to plain text + # Custom anvil will prioritise format that result is a smaller resulting text + # Note that not allowing certain format will lead to some lost of color or tags. + # If configuration are exact as append appending this paper should result in the exact same color + # + # Color code will be prefixed by "&" and hexadecimal color by "#". + # If color code is allowed, "&" in the text will get converted to "&&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + +# Allow to replace the xp cost by a monetary cost +# If enabled it will not be bound to the experience level limits +# +# It also requires to enable dialog rename (set "enable_dialog_rename: false" a bit higher) +# If dialog rename permission is enabled and player do not have the permission merge will fall back to vanilla xp cost +# +# If you are using custom craft I recommend using Linear Xp Cost with Exact Linear Xp as normal Xp Cost will act "weird" +# But Linear Xp will act as 1$ time global multiplier. In other word: like you expect +# +# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 and later +monetary_cost: + enabled: false + # If using vault unlocked this allow to specify what currency should be used for anvil usage + # default being the default currency + currency: default + # multiply the anvil cost by a value to allow to have price a big bigger than like 40 + multipliers: + # global multipliers. all usage type will be multiplied by this value + global: 1.0 + # usage specific type. it will only apply for specific xp "reason" + enchantment: 1.0 # related to enchantments level + repair: 1.0 # for repairing via unit repair (per unit) + rename: 1.0 # for renaming the item + lore_edit: 1.0 # for changing the lore of the item (only if lore edit is enabled) + work_penalty: 1.0 # for work penalty (aka use penalty) + recipe: 1.0 # for custom anvil recipe cost + +# Whether to show debug logging +debug_log: false + +# Whether to show verbose debug logging +debug_log_verbose: false + +configVersion: 1.11.0 diff --git a/defaultconfigs/1.18/custom_recipes.yml b/defaultconfigs/1.18/custom_recipes.yml new file mode 100644 index 0000000..57c2220 --- /dev/null +++ b/defaultconfigs/1.18/custom_recipes.yml @@ -0,0 +1,5 @@ +# ---------------------------------------------------- +# This config file is to store custom craft +# It is recommended to use the in game config editor for this configuration. +# /customanvilconfig With ca.config.edit permission +# ---------------------------------------------------- diff --git a/defaultconfigs/1.18/enchant_conflict.yml b/defaultconfigs/1.18/enchant_conflict.yml new file mode 100644 index 0000000..45d62c3 --- /dev/null +++ b/defaultconfigs/1.18/enchant_conflict.yml @@ -0,0 +1,247 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# material conflicts +# +# If you want to edit this file: +# - A conflict will apply to every item except if in one of the notAffectedGroups group +# - the conflict will count only if the user try to combine at least as +# many conflicting enchantment as "maxEnchantmentBeforeConflict" +# +# +# ---------------------------------------------------- +# These restriction are about not allowing enchantment +# on illegal items +# ---------------------------------------------------- + +restriction_aqua_affinity: + enchantments: [ minecraft:aqua_affinity ] + notAffectedGroups: [ enchanted_book, helmets ] + +restriction_bane_of_arthropods: + enchantments: [ minecraft:bane_of_arthropods ] + notAffectedGroups: [ enchanted_book, melee_weapons ] + +restriction_blast_protection: + enchantments: [ minecraft:blast_protection ] + notAffectedGroups: [ enchanted_book, armors ] + +restriction_channeling: + enchantments: [ minecraft:channeling ] + notAffectedGroups: [ enchanted_book, trident ] + +restriction_binding_curse: + enchantments: [ minecraft:binding_curse ] + notAffectedGroups: [ enchanted_book, wearable ] + +restriction_vanishing_curse: + enchantments: [ minecraft:vanishing_curse ] + notAffectedGroups: [ enchanted_book, can_vanish ] + +restriction_depth_strider: + enchantments: [ minecraft:depth_strider ] + notAffectedGroups: [ enchanted_book, boots ] + +restriction_efficiency: + enchantments: [ minecraft:efficiency ] + notAffectedGroups: [ enchanted_book, tools, shears ] + +restriction_feather_falling: + enchantments: [ minecraft:feather_falling ] + notAffectedGroups: [ enchanted_book, boots ] + +restriction_fire_aspect: + enchantments: [ minecraft:fire_aspect ] + notAffectedGroups: [ enchanted_book, swords ] + +restriction_fire_protection: + enchantments: [ minecraft:fire_protection ] + notAffectedGroups: [ enchanted_book, armors ] + +restriction_flame: + enchantments: [ minecraft:flame ] + notAffectedGroups: [ enchanted_book, bow ] + +restriction_fortune: + enchantments: [ minecraft:fortune ] + notAffectedGroups: [ enchanted_book, tools ] + +restriction_frost_walker: + enchantments: [ minecraft:frost_walker ] + notAffectedGroups: [ enchanted_book, boots ] + +restriction_impaling: + enchantments: [ minecraft:impaling ] + notAffectedGroups: [ enchanted_book, trident ] + +restriction_infinity: + enchantments: [ minecraft:infinity ] + notAffectedGroups: [ enchanted_book, bow ] + +restriction_knockback: + enchantments: [ minecraft:knockback ] + notAffectedGroups: [ enchanted_book, swords ] + +restriction_looting: + enchantments: [ minecraft:looting ] + notAffectedGroups: [ enchanted_book, swords ] + +restriction_loyalty: + enchantments: [ minecraft:loyalty ] + notAffectedGroups: [ enchanted_book, trident ] + +restriction_luck_of_the_sea: + enchantments: [ minecraft:luck_of_the_sea ] + notAffectedGroups: [ enchanted_book, fishing_rod ] + +restriction_lure: + enchantments: [ minecraft:lure ] + notAffectedGroups: [ enchanted_book, fishing_rod ] + +restriction_mending: + enchantments: [ minecraft:mending ] + notAffectedGroups: [ enchanted_book, can_unbreak ] + +restriction_minecraft_multishot: + enchantments: [ minecraft:multishot ] + notAffectedGroups: [ enchanted_book, crossbow ] + +restriction_piercing: + enchantments: [ minecraft:piercing ] + notAffectedGroups: [ enchanted_book, crossbow ] + +restriction_power: + enchantments: [ minecraft:power ] + notAffectedGroups: [ enchanted_book, bow ] + +restriction_projectile_protection: + enchantments: [ minecraft:projectile_protection ] + notAffectedGroups: [ enchanted_book, armors ] + +restriction_protection: + enchantments: [ minecraft:protection ] + notAffectedGroups: [ enchanted_book, armors ] + +restriction_punch: + enchantments: [ minecraft:punch ] + notAffectedGroups: [ enchanted_book, bow ] + +restriction_quick_charge: + enchantments: [ minecraft:quick_charge ] + notAffectedGroups: [ enchanted_book, crossbow ] + +restriction_respiration: + enchantments: [ minecraft:respiration ] + notAffectedGroups: [ enchanted_book, helmets ] + +restriction_riptide: + enchantments: [ minecraft:riptide ] + notAffectedGroups: [ enchanted_book, trident ] + +restriction_sharpness: + enchantments: [ minecraft:sharpness ] + notAffectedGroups: [ enchanted_book, melee_weapons ] + +restriction__silk_touch: + enchantments: [ minecraft:silk_touch ] + notAffectedGroups: [ enchanted_book, tools ] + +restriction_smite: + enchantments: [ minecraft:smite ] + notAffectedGroups: [ enchanted_book, melee_weapons ] + +restriction_soul_speed: + enchantments: [ minecraft:soul_speed ] + notAffectedGroups: [ enchanted_book, boots ] + +restriction_sweeping_edge: + enchantments: [ minecraft:sweeping, minecraft:sweeping_edge ] + notAffectedGroups: [ enchanted_book, swords ] + +# Do not exist in 1.18, that mean useInFuture will be set to true +# useInFuture set to true also mean it will not warn if there is an issue +restriction_swift_sneak: + useInFuture: true + enchantments: [ minecraft:swift_sneak ] + notAffectedGroups: [ enchanted_book, leggings ] + +restriction_thorns: + enchantments: [ minecraft:thorns ] + notAffectedGroups: [ enchanted_book, armors ] + +restriction__unbreaking: + enchantments: [ minecraft:unbreaking ] + notAffectedGroups: [ enchanted_book, can_unbreak ] + +# ---------------------------------------------------- +# Now we have conflicts about enchantment Incompatibility +# We just filtered what item enchantments can be applied +# notAffectedGroups is empty as we don't want anything to not respect theses rules +# maxEnchantmentBeforeConflict is set to 1 to only have 1 on those enchantment available +# ---------------------------------------------------- + +sword_enchant_conflict: + enchantments: + - minecraft:bane_of_arthropods + - minecraft:smite + - minecraft:sharpness + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +protection_enchant_conflict: + enchantments: + - minecraft:blast_protection + - minecraft:fire_protection + - minecraft:projectile_protection + - minecraft:protection + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +trident_conflict1: + enchantments: + - minecraft:channeling + - minecraft:riptide + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +trident_conflict2: + enchantments: + - minecraft:loyalty + - minecraft:riptide + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +boot_conflict: + enchantments: + - minecraft:depth_strider + - minecraft:frost_walker + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +tool_conflict: + enchantments: + - minecraft:fortune + - minecraft:silk_touch + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +bow_conflict: + enchantments: + - minecraft:mending + - minecraft:infinity + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +crossbow_conflict: + enchantments: + - minecraft:multishot + - minecraft:piercing + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +# ---------------------------------------------------- +# Bellow is for custom conflicts. +# This is also where conflict create from the gui will be placed. +# ---------------------------------------------------- diff --git a/defaultconfigs/1.18/item_groups.yml b/defaultconfigs/1.18/item_groups.yml new file mode 100644 index 0000000..3c1eb5d --- /dev/null +++ b/defaultconfigs/1.18/item_groups.yml @@ -0,0 +1,210 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# Please note this config use spigot material names. +# It should match minecraft name in most case, maybe every case, but I can't be sure +# In case there an issue with material name, you can found them here: +# https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html + +# An empty Exclude group exclude nothing, so it contain everything +everything: + type: exclude + +# An empty include group will include nothing +nothing: + type: include + +# This group is an example of a group including only stone and polished granite +example_include: + type: include + items: + - stone + - polished_granite + +# This group contain everything except polished granite and elements of example_include +example_exclude: + type: exclude + items: + - polished_granite + groups: + - example_include + +# Default configuration should be vanilla enchantment conflict group +# there may have error, if you find one you can fix it ! +# https://minecraft.fandom.com/wiki/Enchanting + +swords: + type: include + items: + - wooden_sword + - stone_sword + - iron_sword + - diamond_sword + - golden_sword + - netherite_sword + +axes: + type: include + items: + - wooden_axe + - stone_axe + - iron_axe + - diamond_axe + - golden_axe + - netherite_axe + +melee_weapons: + type: include + groups: + - swords + - axes + +helmets: + type: include + items: + - leather_helmet + - chainmail_helmet + - iron_helmet + - diamond_helmet + - golden_helmet + - netherite_helmet + - turtle_helmet + +chestplate: + type: include + items: + - leather_chestplate + - chainmail_chestplate + - iron_chestplate + - diamond_chestplate + - golden_chestplate + - netherite_chestplate + +leggings: + type: include + items: + - leather_leggings + - chainmail_leggings + - iron_leggings + - diamond_leggings + - golden_leggings + - netherite_leggings + +boots: + type: include + items: + - leather_boots + - chainmail_boots + - iron_boots + - diamond_boots + - golden_boots + - netherite_boots + +armors: + type: include + groups: + - helmets + - chestplate + - leggings + - boots + +wearable: + type: include + items: + - elytra + - carved_pumpkin + - skeleton_skull + - wither_skeleton_skull + - zombie_head + - player_head + - creeper_head + - dragon_head + # do not exist in 1.18 but exist in future update + - piglin_head + groups: + - armors + +tools: + type: include + items: + - wooden_pickaxe + - stone_pickaxe + - iron_pickaxe + - diamond_pickaxe + - golden_pickaxe + - netherite_pickaxe + - wooden_shovel + - stone_shovel + - iron_shovel + - diamond_shovel + - golden_shovel + - netherite_shovel + - wooden_hoe + - stone_hoe + - iron_hoe + - diamond_hoe + - golden_hoe + - netherite_hoe + groups: + - axes + +enchanted_book: + type: include + items: + - enchanted_book + +trident: + type: include + items: + - trident + +bow: + type: include + items: + - bow + +crossbow: + type: include + items: + - crossbow + +fishing_rod: + type: include + items: + - fishing_rod + +shears: + type: include + items: + - shears + +can_unbreak: + type: include + items: + - elytra + - flint_and_steel + - shield + - carrot_on_a_stick + - warped_fungus_on_a_stick + # do not exist in 1.18 but exist in future update + - brush + groups: + - melee_weapons + - tools + - armors + - trident + - bow + - crossbow + - fishing_rod + - shears + +can_vanish: + type: include + items: + - compass + groups: + - wearable + - can_unbreak + diff --git a/defaultconfigs/1.18/unit_repair_item.yml b/defaultconfigs/1.18/unit_repair_item.yml new file mode 100644 index 0000000..2902cce --- /dev/null +++ b/defaultconfigs/1.18/unit_repair_item.yml @@ -0,0 +1,190 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# Unit repair configuration +# +# This configuration is to make custom unit repair +# A unit repair is, for example, a diamond to repair a diamond sword +# In vanilla, a unit repair 25% of object durability +# you can make a custom value here +# +# Item name should NOT combine caps and no caps (example: Stone) + +# Default value if the config is an invalid value (value <= 0 ) +# If value > 1 it will be treated as being = 1 +default_repair_amount: 0.25 + +# You can add custom unit repair +# The example bellow make a shield repaired by 10% by sticks + +#stick: +# shield: 0.10 + + +# Vanilla unit repair group is bellow +diamond: + diamond_helmet: 0.25 + diamond_chestplate: 0.25 + diamond_leggings: 0.25 + diamond_boots: 0.25 + diamond_sword: 0.25 + diamond_pickaxe: 0.25 + diamond_axe: 0.25 + diamond_shovel: 0.25 + diamond_hoe: 0.25 + +netherite_ingot: + netherite_helmet: 0.25 + netherite_chestplate: 0.25 + netherite_leggings: 0.25 + netherite_boots: 0.25 + netherite_sword: 0.25 + netherite_pickaxe: 0.25 + netherite_axe: 0.25 + netherite_shovel: 0.25 + netherite_hoe: 0.25 + +gold_ingot: + golden_helmet: 0.25 + golden_chestplate: 0.25 + golden_leggings: 0.25 + golden_boots: 0.25 + golden_sword: 0.25 + golden_pickaxe: 0.25 + golden_axe: 0.25 + golden_shovel: 0.25 + golden_hoe: 0.25 + +iron_ingot: + iron_helmet: 0.25 + iron_chestplate: 0.25 + iron_leggings: 0.25 + iron_boots: 0.25 + iron_sword: 0.25 + iron_pickaxe: 0.25 + iron_axe: 0.25 + iron_shovel: 0.25 + iron_hoe: 0.25 + +cobblestone: + stone_sword: 0.25 + stone_pickaxe: 0.25 + stone_axe: 0.25 + stone_shovel: 0.25 + stone_hoe: 0.25 + +cobbled_deepslate: + stone_sword: 0.25 + stone_pickaxe: 0.25 + stone_axe: 0.25 + stone_shovel: 0.25 + stone_hoe: 0.25 + +blackstone: + stone_sword: 0.25 + stone_pickaxe: 0.25 + stone_axe: 0.25 + stone_shovel: 0.25 + stone_hoe: 0.25 + +leather: + leather_helmet: 0.25 + leather_chestplate: 0.25 + leather_leggings: 0.25 + leather_boots: 0.25 + +phantom_membrane: + elytra: 0.25 + +scute: + turtle_helmet: 0.25 + +oak_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +spruce_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +birch_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +jungle_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +acacia_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +dark_oak_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +mangrove_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +cherry_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +bamboo_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +crimson_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +warped_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 diff --git a/defaultconfigs/1.21.11/README.md b/defaultconfigs/1.21.11/README.md new file mode 100644 index 0000000..a33a09b --- /dev/null +++ b/defaultconfigs/1.21.11/README.md @@ -0,0 +1,6 @@ +### Default Plugin's Configurations For 1.21.11 +- [config.yml](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21.11/config.yml) +- [enchant_conflict.yml](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21.11/enchant_conflict.yml) +- [item_groups.yml](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21.11/item_groups.yml) +- [unit_repair_item.yml](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21.11/unit_repair_item.yml) +- [custom_recipes.yml](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21.11/custom_recipes.yml) diff --git a/defaultconfigs/1.21.11/config.yml b/defaultconfigs/1.21.11/config.yml new file mode 100644 index 0000000..44885b7 --- /dev/null +++ b/defaultconfigs/1.21.11/config.yml @@ -0,0 +1,487 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# What service of metric should custom anvil use +# Custom anvil collect generic information like server minecraft version, type, etc... +# It can also collect error information if error is happening (currently faststats only) +# It can also be disabled +# Please refer to README for public metric link +# Possible options: auto, bstat, faststats, disabled (auto by default) +metric_type: auto + +# Allow to report errors made caused by this plugin (only for faststats) +# This allows me to fix potentials issue that I'm not aware of +# Accept true or false (true by default) +metric_collect_errors: true + +# All anvil cost will be capped to limit_repair_value if enabled. +# +# In other words: +# For any anvil cost greater than limit_repair_value, Cost will be set to limit_repair_value. +limit_repair_cost: false + +# Max cost value the Anvil can get to. +# +# Valid values include 0 to 1000. +# Cost will be displayed as "Too Expensive": +# - If Cost is above 39 +# - And replace_too_expensive is disabled (false) +limit_repair_value: 39 + +# Whether the anvil's cost limit should be removed entirely. +# +# The anvil will still visually display "Too Expensive" if "replace_too_expensive" is disabled +# However, the action will be completable if xp requirement is meet. +remove_repair_limit: false + +# Whenever anvil cost is above 39 should display the true price and not "Too Expensive". +# +# However, when bypassing "Too Expensive", anvil price will be displayed as Green. +# If the action is not completable, the cost will still be displayed as "Too expensive". +# That mean you also need to change other settings like remove_repair_limit or limit_repair_cost. +# +# Require ProtocoLib. +replace_too_expensive: false + +# XP Level amount added to the anvil when the item is repaired by another item of the same type +# +# Valid values include 0 to 1000 +item_repair_cost: 2 + +# XP Level amount added to the anvil when the item is renamed +# +# Valid values include 0 to 1000 +item_rename_cost: 1 + +# XP Level amount added to the anvil when the item is repaired by an "unit" +# For example: a Diamond on a Diamond Sword +# What's considered unit for what can be edited on the unit repair configuration. +# +# Valid values include 0 to 1000 +unit_repair_cost: 1 + +# XP Level amount added to the anvil when a sacrifice enchantment +# conflict with one of the left item enchantment +# +# Valid values include 0 to 1000 +sacrifice_illegal_enchant_cost: 1 + +# Allow using color code and hexadecimal color. +# +# Color code are prefixed by "&" and hexadecimal color by "#". +# Color code will not be applied if it colors nothing. "&&" can be used to write "&". +# For minimessage search for minimessage formating https://docs.papermc.io/adventure/minimessage/format/ +# Note that only color and decoration tags are allowed for minimisage in the v1 version of this plugin +# but any global tag will be allowed later when v2 release +allow_color_code: false +allow_hexadecimal_color: false +allow_minimessage: false + +# This enables restricting color code for player having specific permission +# It requires allow_color_code enabled for... obvious reasons +# +# For example: if player want to use "&aHello" it will be required that the player has +# the permission "ca.color.code.a" as he used the color code "a" +# In general permission to give to the player is "ca.color.code.[code]" +# where [code] is the color code you wish to allow the player +# +# It is kinda of useless when minimessage is supported as players would be able to bypass +# that using the equivalent minimessage tag +per_color_code_permission: false + +# Toggle if color should only be applicable if the player a certain permission. +# +# permission are "ca.color.code" for use of color code and "ca.color.hex" for use of hexadecimal color. +permission_needed_for_color: true + +# Xp cost if the player use color in the items name on rename. +# +# Valid values include 0 to 1000. +use_of_color_cost: 0 + +# Dialogue rename menu make use of dialog menu to allow bigger rename +# You can also change the maximum size and set it to -1 or less for maximum +# +# This feature only work on paper 1.21.7 or later +# +# At the moment only english is available for this menu... sorry ! +# +# CustomAnvil use "ca.rename.dialog" when permission +enable_dialog_rename: false +dialog_rename_max_size: 256 +permission_needed_for_dialog_rename: false + +# This allows custom anvil to not "guess" the text used for rename but store it in the item +# It will make item stackable only and only if it had used the same rename text +# +# For practical reason. this only work when dialog rename is enabled +dialog_rename_keep_user_text: true + +# Override limits for specific enchants +# +# Enchantments not listed here will use the value of default_limit +# +# Overrides provided default from aqua_affinity to depth_strider won't change effect with extra levels +# +# Valid range of 0 - 255 for each enchantment +# -1 mean keep default +enchant_limits: + minecraft:aqua_affinity: 1 + minecraft:binding_curse: 1 + minecraft:channeling: 1 + minecraft:flame: 1 + minecraft:infinity: 1 + minecraft:mending: 1 + minecraft:multishot: 1 + minecraft:silk_touch: 1 + minecraft:vanishing_curse: 1 + minecraft:depth_strider: 3 # anything more than 3 is treated as 3 by the game + minecraft:protection: 4 + minecraft:fire_protection: 4 + minecraft:blast_protection: 4 + minecraft:projectile_protection: 4 + minecraft:feather_falling: 4 + minecraft:thorns: 3 + minecraft:respiration: 3 + minecraft:sharpness: 5 + minecraft:smite: 5 + minecraft:bane_of_arthropods: 5 + minecraft:knockback: 2 + minecraft:fire_aspect: 2 + minecraft:looting: 3 + minecraft:sweeping: 3 + minecraft:sweeping_edge: 3 + minecraft:efficiency: 5 + minecraft:unbreaking: 3 + minecraft:fortune: 3 + minecraft:power: 5 + minecraft:punch: 2 + minecraft:luck_of_the_sea: 3 + minecraft:lure: 3 + minecraft:frost_walker: 2 + minecraft:impaling: 5 + minecraft:riptide: 3 + minecraft:loyalty: 3 + minecraft:piercing: 4 + minecraft:quick_charge: 3 + minecraft:soul_speed: 3 + minecraft:swift_sneak: 3 + minecraft:density: 5 + minecraft:breach: 4 + minecraft:wind_burst: 3 + minecraft:lunge: 3 + +# Multipliers used to calculate the enchantment's value in repair/combining +# +# Values here are pulled from the fandom wiki: +# https://minecraft.fandom.com/wiki/Anvil_mechanics#Costs_for_combining_enchantments +# +# If an enchantment is missing values here, or is less than 0, it will default to 0 +# +# Calculated as: [Enchantment lvl] * [multiplier] +# +# With default values protection 4 would have a value of 4 when +# coming from either a book (4 * 1) or an item (4 * 1) +enchant_values: + minecraft:aqua_affinity: + item: 4 + book: 2 + minecraft:bane_of_arthropods: + item: 2 + book: 1 + minecraft:binding_curse: + item: 8 + book: 4 + minecraft:blast_protection: + item: 4 + book: 2 + minecraft:channeling: + item: 8 + book: 4 + minecraft:depth_strider: + item: 4 + book: 2 + minecraft:efficiency: + item: 1 + book: 1 + minecraft:flame: + item: 4 + book: 2 + minecraft:feather_falling: + item: 2 + book: 1 + minecraft:fire_aspect: + item: 4 + book: 2 + minecraft:fire_protection: + item: 2 + book: 1 + minecraft:fortune: + item: 4 + book: 2 + minecraft:frost_walker: + item: 4 + book: 2 + minecraft:impaling: + item: 4 + book: 2 + minecraft:infinity: + item: 8 + book: 4 + minecraft:knockback: + item: 2 + book: 1 + minecraft:looting: + item: 4 + book: 2 + minecraft:loyalty: + item: 1 + book: 1 + minecraft:luck_of_the_sea: + item: 4 + book: 2 + minecraft:lure: + item: 4 + book: 2 + minecraft:mending: + item: 4 + book: 2 + minecraft:multishot: + item: 4 + book: 2 + minecraft:piercing: + item: 1 + book: 1 + minecraft:power: + item: 1 + book: 1 + minecraft:projectile_protection: + item: 2 + book: 1 + minecraft:protection: + item: 1 + book: 1 + minecraft:punch: + item: 4 + book: 2 + minecraft:quick_charge: + item: 2 + book: 1 + minecraft:respiration: + item: 4 + book: 2 + minecraft:riptide: + item: 4 + book: 2 + minecraft:silk_touch: + item: 8 + book: 4 + minecraft:sharpness: + item: 1 + book: 1 + minecraft:smite: + item: 2 + book: 1 + minecraft:soul_speed: + item: 8 + book: 4 + minecraft:swift_sneak: + item: 8 + book: 4 + minecraft:sweeping: + item: 4 + book: 2 + minecraft:sweeping_edge: + item: 4 + book: 2 + minecraft:thorns: + item: 8 + book: 4 + minecraft:unbreaking: + item: 2 + book: 1 + minecraft:vanishing_curse: + item: 8 + book: 4 + minecraft:density: + item: 2 + book: 1 + minecraft:breach: + item: 4 + book: 2 + minecraft:wind_burst: + item: 4 + book: 2 + minecraft:lunge: + item: 2 + book: 1 + +# Disable enchantment merging for level above the set value +# Enchantment merging is when, for example, 2 unbreaking II book combine to give sharpness III +# But Enchantment above this value can still be applied. following the previous example, we could still apply a unbreaking III book to a sword +# Even if disable-merge-over of unbreaking is set to 2 +# -1 mean enchantment merge for this enchantment is not disabled. default to -1 if absent. +disable-merge-over: + # Sharpness is set to -1. it equivalent to it not being set to anything (and work as vanilla on default configuration) + minecraft:sharpness: -1 +# If uncommented. 2 unbreaking II book would not give an unbreaking III book. but unbreaking III book can still be applied +# minecraft:unbreaking: 2 + +# The maximum number of enchantment an item can get. -1 for infinity +# Use eco enchant enchant_limit if present by default unless "default" is not equal to -1 +enchantment_count_limit: + default: -1 + # Limit for specific items. example bellow is an example with stick + # Per item enchantment limit override eco enchant enchant_limit and default limit + items: + stick: -1 + +# Settings for lore modification +lore_edit: + book_and_quil: + # Permission is ca.lore_edit.book + use_permission: true + append: + # If adding lore using book & quil is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Cost used for every lore line added + per_line_cost: 0 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If adding the lore consume the book & quil + do_consume: false + # Allow using color code and hexadecimal color when editing lore via book & quil + # + # Color code are prefixed by "&" and hexadecimal color by "#" + # Color code will not be applied if it colors nothing. "&&" can be used to write "&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + # + # Note that currently minimessage would disable hex code when adding color + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + use_cost: 0 + + remove: + # If removing lore using book & quil is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Cost used for every lore line removed + per_line_cost: 0 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If removing the lore consume the book & quil + do_consume: false + # Cost of replacing colors + remove_color_cost: 0 + # Allowed some color and tags to be reverted to plain text + # Custom anvil will prioritise format that result is a smaller resulting text + # Note that not allowing certain format will lead to some lost of color or tags. + # If configuration are exact as append appending this book should result in the exact same color + # + # Color code will be prefixed by "&" and hexadecimal color by "#". + # If color code is allowed, "&" in the text will get converted to "&&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + + paper: + # Permission is ca.lore_edit.paper + use_permission: true + # what order should the lines should get added/removed (start/end, if invalid or not present will be end) + order: end + + append_line: + # If adding lore line using paper is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If adding the lore line consume the paper + do_consume: false + # Allow using color code and hexadecimal color when editing lore via book & quil + # + # Color code are prefixed by "&" and hexadecimal color by "#" + # Color code will not be applied if it colors nothing. "&&" can be used to write "&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + # + # Note that currently minimessage would disable hex code when adding color + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + color_use_cost: 0 + use_cost: 0 + + remove_line: + # If removing lore line using paper is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If removing the lore line consume the paper + do_consume: false + # Cost of replacing colors + remove_color_cost: 0 + # Allowed some color and tags to be reverted to plain text + # Custom anvil will prioritise format that result is a smaller resulting text + # Note that not allowing certain format will lead to some lost of color or tags. + # If configuration are exact as append appending this paper should result in the exact same color + # + # Color code will be prefixed by "&" and hexadecimal color by "#". + # If color code is allowed, "&" in the text will get converted to "&&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + +# Allow to replace the xp cost by a monetary cost +# If enabled it will not be bound to the experience level limits +# +# It also requires to enable dialog rename (set "enable_dialog_rename: false" a bit higher) +# If dialog rename permission is enabled and player do not have the permission merge will fall back to vanilla xp cost +# +# If you are using custom craft I recommend using Linear Xp Cost with Exact Linear Xp as normal Xp Cost will act "weird" +# But Linear Xp will act as 1$ time global multiplier. In other word: like you expect +# +# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 and later +monetary_cost: + enabled: false + # If using vault unlocked this allow to specify what currency should be used for anvil usage + # default being the default currency + currency: default + # multiply the anvil cost by a value to allow to have price a big bigger than like 40 + multipliers: + # global multipliers. all usage type will be multiplied by this value + global: 1.0 + # usage specific type. it will only apply for specific xp "reason" + enchantment: 1.0 # related to enchantments level + repair: 1.0 # for repairing via unit repair (per unit) + rename: 1.0 # for renaming the item + lore_edit: 1.0 # for changing the lore of the item (only if lore edit is enabled) + work_penalty: 1.0 # for work penalty (aka use penalty) + recipe: 1.0 # for custom anvil recipe cost + +# Whether to show debug logging +debug_log: false + +# Whether to show verbose debug logging +debug_log_verbose: false + +configVersion: 1.15.5 +lowMinecraftVersion: 1.21.11 diff --git a/defaultconfigs/1.21.11/custom_recipes.yml b/defaultconfigs/1.21.11/custom_recipes.yml new file mode 100644 index 0000000..57c2220 --- /dev/null +++ b/defaultconfigs/1.21.11/custom_recipes.yml @@ -0,0 +1,5 @@ +# ---------------------------------------------------- +# This config file is to store custom craft +# It is recommended to use the in game config editor for this configuration. +# /customanvilconfig With ca.config.edit permission +# ---------------------------------------------------- diff --git a/defaultconfigs/1.21.11/enchant_conflict.yml b/defaultconfigs/1.21.11/enchant_conflict.yml new file mode 100644 index 0000000..3d62dae --- /dev/null +++ b/defaultconfigs/1.21.11/enchant_conflict.yml @@ -0,0 +1,398 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# material conflicts +# +# If you want to edit this file: +# - A conflict will apply to every item except if in one of the notAffectedGroups group +# - the conflict will count only if the user try to combine at least as +# many conflicting enchantment as "maxEnchantmentBeforeConflict" +# +# +# ---------------------------------------------------- +# These restriction are about not allowing enchantment +# on illegal items +# ---------------------------------------------------- + +restriction_aqua_affinity: + enchantments: + - minecraft:aqua_affinity + notAffectedGroups: + - enchanted_book + - helmets + +restriction_bane_of_arthropods: + enchantments: + - minecraft:bane_of_arthropods + notAffectedGroups: + - enchanted_book + - melee_weapons + - mace + +restriction_blast_protection: + enchantments: + - minecraft:blast_protection + notAffectedGroups: + - enchanted_book + - armors + +restriction_channeling: + enchantments: + - minecraft:channeling + notAffectedGroups: + - enchanted_book + - trident + +restriction_binding_curse: + enchantments: + - minecraft:binding_curse + notAffectedGroups: + - enchanted_book + - wearable + +restriction_vanishing_curse: + enchantments: + - minecraft:vanishing_curse + notAffectedGroups: + - enchanted_book + - can_vanish + +restriction_depth_strider: + enchantments: + - minecraft:depth_strider + notAffectedGroups: + - enchanted_book + - boots + +restriction_efficiency: + enchantments: + - minecraft:efficiency + notAffectedGroups: + - enchanted_book + - tools + - shears + +restriction_feather_falling: + enchantments: + - minecraft:feather_falling + notAffectedGroups: + - enchanted_book + - boots + +restriction_fire_aspect: + enchantments: + - minecraft:fire_aspect + notAffectedGroups: + - enchanted_book + - swords + - mace + - spears + +restriction_fire_protection: + enchantments: + - minecraft:fire_protection + notAffectedGroups: + - enchanted_book + - armors + +restriction_flame: + enchantments: + - minecraft:flame + notAffectedGroups: + - enchanted_book + - bow + +restriction_fortune: + enchantments: + - minecraft:fortune + notAffectedGroups: + - enchanted_book + - tools + +restriction_frost_walker: + enchantments: + - minecraft:frost_walker + notAffectedGroups: + - enchanted_book + - boots + +restriction_impaling: + enchantments: + - minecraft:impaling + notAffectedGroups: + - enchanted_book + - trident + +restriction_infinity: + enchantments: + - minecraft:infinity + notAffectedGroups: + - enchanted_book + - bow + +restriction_knockback: + enchantments: + - minecraft:knockback + notAffectedGroups: + - enchanted_book + - swords + - spears + +restriction_looting: + enchantments: + - minecraft:looting + notAffectedGroups: + - enchanted_book + - swords + - spears + +restriction_loyalty: + enchantments: + - minecraft:loyalty + notAffectedGroups: + - enchanted_book + - trident + +restriction_luck_of_the_sea: + enchantments: + - minecraft:luck_of_the_sea + notAffectedGroups: + - enchanted_book + - fishing_rod + +restriction_lure: + enchantments: + - minecraft:lure + notAffectedGroups: + - enchanted_book + - fishing_rod + +restriction_mending: + enchantments: + - minecraft:mending + notAffectedGroups: + - enchanted_book + - can_unbreak + +restriction_minecraft_multishot: + enchantments: + - minecraft:multishot + notAffectedGroups: + - enchanted_book + - crossbow + +restriction_piercing: + enchantments: + - minecraft:piercing + notAffectedGroups: + - enchanted_book + - crossbow + +restriction_power: + enchantments: + - minecraft:power + notAffectedGroups: + - enchanted_book + - bow + +restriction_projectile_protection: + enchantments: + - minecraft:projectile_protection + notAffectedGroups: + - enchanted_book + - armors + +restriction_protection: + enchantments: + - minecraft:protection + notAffectedGroups: + - enchanted_book + - armors + +restriction_punch: + enchantments: + - minecraft:punch + notAffectedGroups: + - enchanted_book + - bow + +restriction_quick_charge: + enchantments: + - minecraft:quick_charge + notAffectedGroups: + - enchanted_book + - crossbow + +restriction_respiration: + enchantments: + - minecraft:respiration + notAffectedGroups: + - enchanted_book + - helmets + +restriction_riptide: + enchantments: + - minecraft:riptide + notAffectedGroups: + - enchanted_book + - trident + +restriction_sharpness: + enchantments: + - minecraft:sharpness + notAffectedGroups: + - enchanted_book + - melee_weapons + +restriction__silk_touch: + enchantments: + - minecraft:silk_touch + notAffectedGroups: + - enchanted_book + - tools + +restriction_smite: + enchantments: + - minecraft:smite + notAffectedGroups: + - enchanted_book + - melee_weapons + - mace + +restriction_soul_speed: + enchantments: + - minecraft:soul_speed + notAffectedGroups: + - enchanted_book + - boots + +restriction_sweeping_edge: + enchantments: + - minecraft:sweeping + - minecraft:sweeping_edge + notAffectedGroups: + - enchanted_book + - swords + +# Do not exist in 1.18, that mean useInFuture will be set to true +# useInFuture set to true also mean it will not warn if there is an issue +restriction_swift_sneak: + useInFuture: true + enchantments: + - minecraft:swift_sneak + notAffectedGroups: + - enchanted_book + - leggings + +restriction_thorns: + enchantments: + - minecraft:thorns + notAffectedGroups: + - enchanted_book + - armors + +restriction__unbreaking: + enchantments: + - minecraft:unbreaking + notAffectedGroups: + - enchanted_book + - can_unbreak + +# ---------------------------------------------------- +# Now we have conflicts about enchantment Incompatibility +# We just filtered what item enchantments can be applied +# notAffectedGroups is empty as we don't want anything to not respect theses rules +# maxEnchantmentBeforeConflict is set to 1 to only have 1 on those enchantment available +# ---------------------------------------------------- + +sword_enchant_conflict: + enchantments: + - minecraft:bane_of_arthropods + - minecraft:smite + - minecraft:sharpness + - minecraft:density + - minecraft:breach + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 + +protection_enchant_conflict: + enchantments: + - minecraft:blast_protection + - minecraft:fire_protection + - minecraft:projectile_protection + - minecraft:protection + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 + +trident_conflict1: + enchantments: + - minecraft:channeling + - minecraft:riptide + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 + +trident_conflict2: + enchantments: + - minecraft:loyalty + - minecraft:riptide + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 + +boot_conflict: + enchantments: + - minecraft:depth_strider + - minecraft:frost_walker + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 + +tool_conflict: + enchantments: + - minecraft:fortune + - minecraft:silk_touch + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 + +bow_conflict: + enchantments: + - minecraft:mending + - minecraft:infinity + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 + +crossbow_conflict: + enchantments: + - minecraft:multishot + - minecraft:piercing + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 +restriction_density: + enchantments: + - minecraft:density + notAffectedGroups: + - mace + - enchanted_book +restriction_breach: + enchantments: + - minecraft:breach + notAffectedGroups: + - mace + - enchanted_book +restriction_wind_burst: + enchantments: + - minecraft:wind_burst + notAffectedGroups: + - mace + - enchanted_book +restriction_lunge: + enchantments: + - minecraft:lunge + notAffectedGroups: + - spears + - enchanted_book + +# ---------------------------------------------------- +# Bellow is for custom conflicts. +# This is also where conflict create from the gui will be placed. +# ---------------------------------------------------- diff --git a/defaultconfigs/1.21.11/item_groups.yml b/defaultconfigs/1.21.11/item_groups.yml new file mode 100644 index 0000000..9f2a877 --- /dev/null +++ b/defaultconfigs/1.21.11/item_groups.yml @@ -0,0 +1,247 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# Please note this config use spigot material names. +# It should match minecraft name in most case, maybe every case, but I can't be sure +# In case there an issue with material name, you can found them here: +# https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html + +# An empty Exclude group exclude nothing, so it contain everything +everything: + type: exclude + +# An empty include group will include nothing +nothing: + type: include + +# This group is an example of a group including only stone and polished granite +example_include: + type: include + items: + - stone + - polished_granite + +# This group contain everything except polished granite and elements of example_include +example_exclude: + type: exclude + items: + - polished_granite + groups: + - example_include + +# Default configuration should be vanilla enchantment conflict group +# there may have error, if you find one you can fix it ! +# https://minecraft.fandom.com/wiki/Enchanting + +swords: + type: include + items: + - wooden_sword + - stone_sword + - iron_sword + - diamond_sword + - golden_sword + - netherite_sword + - copper_sword + +axes: + type: include + items: + - wooden_axe + - stone_axe + - iron_axe + - diamond_axe + - golden_axe + - netherite_axe + - copper_axe + +melee_weapons: + type: include + groups: + - swords + - axes + - spears + +helmets: + type: include + items: + - leather_helmet + - chainmail_helmet + - iron_helmet + - diamond_helmet + - golden_helmet + - netherite_helmet + - turtle_helmet + - copper_helmet + +chestplate: + type: include + items: + - leather_chestplate + - chainmail_chestplate + - iron_chestplate + - diamond_chestplate + - golden_chestplate + - netherite_chestplate + - copper_chestplate + +leggings: + type: include + items: + - leather_leggings + - chainmail_leggings + - iron_leggings + - diamond_leggings + - golden_leggings + - netherite_leggings + - copper_leggings + +boots: + type: include + items: + - leather_boots + - chainmail_boots + - iron_boots + - diamond_boots + - golden_boots + - netherite_boots + - copper_boots + +armors: + type: include + groups: + - helmets + - chestplate + - leggings + - boots + +wearable: + type: include + items: + - elytra + - carved_pumpkin + - skeleton_skull + - wither_skeleton_skull + - zombie_head + - player_head + - creeper_head + - dragon_head + - piglin_head + groups: + - armors + +pickaxes: + type: include + items: + - wooden_pickaxe + - stone_pickaxe + - iron_pickaxe + - diamond_pickaxe + - golden_pickaxe + - netherite_pickaxe + - copper_pickaxe + +shovels: + type: include + items: + - wooden_shovel + - stone_shovel + - iron_shovel + - diamond_shovel + - golden_shovel + - netherite_shovel + - copper_shovel + +hoes: + type: include + items: + - wooden_hoe + - stone_hoe + - iron_hoe + - diamond_hoe + - golden_hoe + - netherite_hoe + - copper_hoe + +tools: + type: include + groups: + - pickaxes + - shovels + - hoes + - axes + +enchanted_book: + type: include + items: + - enchanted_book + +trident: + type: include + items: + - trident + +bow: + type: include + items: + - bow + +crossbow: + type: include + items: + - crossbow + +fishing_rod: + type: include + items: + - fishing_rod + +shears: + type: include + items: + - shears + +can_unbreak: + type: include + items: + - elytra + - flint_and_steel + - shield + - carrot_on_a_stick + - warped_fungus_on_a_stick + - brush + groups: + - melee_weapons + - tools + - armors + - trident + - bow + - crossbow + - fishing_rod + - shears + - mace + +can_vanish: + type: include + items: + - compass + groups: + - wearable + - can_unbreak +mace: + type: include + items: + - mace +spears: + type: include + items: + - wooden_spear + - golden_spear + - stone_spear + - copper_spear + - iron_spear + - diamond_spear + - netherite_spear + diff --git a/defaultconfigs/1.21.11/unit_repair_item.yml b/defaultconfigs/1.21.11/unit_repair_item.yml new file mode 100644 index 0000000..ed981b1 --- /dev/null +++ b/defaultconfigs/1.21.11/unit_repair_item.yml @@ -0,0 +1,220 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# Unit repair configuration +# +# This configuration is to make custom unit repair +# A unit repair is, for example, a diamond to repair a diamond sword +# In vanilla, a unit repair 25% of object durability +# you can make a custom value here +# +# Item name should NOT combine caps and no caps (example: Stone) + +# Default value if the config is an invalid value (value <= 0 ) +# If value > 1 it will be treated as being = 1 +default_repair_amount: 0.25 + +# You can add custom unit repair +# The example bellow make a shield repaired by 10% by sticks + +# stick: +# shield: 0.10 + + +# Vanilla unit repair group is bellow +diamond: + diamond_helmet: 0.25 + diamond_chestplate: 0.25 + diamond_leggings: 0.25 + diamond_boots: 0.25 + diamond_sword: 0.25 + diamond_pickaxe: 0.25 + diamond_axe: 0.25 + diamond_shovel: 0.25 + diamond_hoe: 0.25 + diamond_spear: 0.25 + +netherite_ingot: + netherite_helmet: 0.25 + netherite_chestplate: 0.25 + netherite_leggings: 0.25 + netherite_boots: 0.25 + netherite_sword: 0.25 + netherite_pickaxe: 0.25 + netherite_axe: 0.25 + netherite_shovel: 0.25 + netherite_hoe: 0.25 + netherite_spear: 0.25 + +gold_ingot: + golden_helmet: 0.25 + golden_chestplate: 0.25 + golden_leggings: 0.25 + golden_boots: 0.25 + golden_sword: 0.25 + golden_pickaxe: 0.25 + golden_axe: 0.25 + golden_shovel: 0.25 + golden_hoe: 0.25 + golden_spear: 0.25 + +iron_ingot: + iron_helmet: 0.25 + iron_chestplate: 0.25 + iron_leggings: 0.25 + iron_boots: 0.25 + iron_sword: 0.25 + iron_pickaxe: 0.25 + iron_axe: 0.25 + iron_shovel: 0.25 + iron_hoe: 0.25 + iron_spear: 0.25 + +cobblestone: + stone_sword: 0.25 + stone_pickaxe: 0.25 + stone_axe: 0.25 + stone_shovel: 0.25 + stone_hoe: 0.25 + stone_spear: 0.25 + +cobbled_deepslate: + stone_sword: 0.25 + stone_pickaxe: 0.25 + stone_axe: 0.25 + stone_shovel: 0.25 + stone_hoe: 0.25 + stone_spear: 0.25 + +blackstone: + stone_sword: 0.25 + stone_pickaxe: 0.25 + stone_axe: 0.25 + stone_shovel: 0.25 + stone_hoe: 0.25 + +leather: + leather_helmet: 0.25 + leather_chestplate: 0.25 + leather_leggings: 0.25 + leather_boots: 0.25 + +phantom_membrane: + elytra: 0.25 + +scute: + turtle_helmet: 0.25 + +oak_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + wooden_spear: 0.25 + +spruce_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + wooden_spear: 0.25 + +birch_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + wooden_spear: 0.25 + +jungle_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + wooden_spear: 0.25 + +acacia_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + wooden_spear: 0.25 + +dark_oak_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + wooden_spear: 0.25 + +mangrove_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + wooden_spear: 0.25 + +cherry_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + wooden_spear: 0.25 + +bamboo_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + wooden_spear: 0.25 + +crimson_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + wooden_spear: 0.25 + +warped_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + wooden_spear: 0.25 +breeze_rod: + mace: 0.25 +copper_ingot: + copper_helmet: 0.25 + copper_chestplate: 0.25 + copper_leggings: 0.25 + copper_boots: 0.25 + copper_pickaxe: 0.25 + copper_shovel: 0.25 + copper_hoe: 0.25 + copper_axe: 0.25 + copper_sword: 0.25 + copper_spear: 0.25 diff --git a/defaultconfigs/1.21.9/README.md b/defaultconfigs/1.21.9/README.md new file mode 100644 index 0000000..2b3bfa8 --- /dev/null +++ b/defaultconfigs/1.21.9/README.md @@ -0,0 +1,6 @@ +### Default Plugin's Configurations For 1.21.9 +- [config.yml](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21.9/config.yml) +- [enchant_conflict.yml](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21.9/enchant_conflict.yml) +- [item_groups.yml](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21.9/item_groups.yml) +- [unit_repair_item.yml](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21.9/unit_repair_item.yml) +- [custom_recipes.yml](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21.9/custom_recipes.yml) diff --git a/defaultconfigs/1.21.9/config.yml b/defaultconfigs/1.21.9/config.yml new file mode 100644 index 0000000..4df5876 --- /dev/null +++ b/defaultconfigs/1.21.9/config.yml @@ -0,0 +1,479 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# What service of metric should custom anvil use +# Custom anvil collect generic information like server minecraft version, type, etc... +# It can also collect error information if error is happening (currently faststats only) +# It can also be disabled +# Please refer to README for public metric link +# Possible options: auto, bstat, faststats, disabled (auto by default) +metric_type: auto + +# Allow to report errors made caused by this plugin (only for faststats) +# This allows me to fix potentials issue that I'm not aware of +# Accept true or false (true by default) +metric_collect_errors: true + +# All anvil cost will be capped to limit_repair_value if enabled. +# +# In other words: +# For any anvil cost greater than limit_repair_value, Cost will be set to limit_repair_value. +limit_repair_cost: false + +# Max cost value the Anvil can get to. +# +# Valid values include 0 to 1000. +# Cost will be displayed as "Too Expensive": +# - If Cost is above 39 +# - And replace_too_expensive is disabled (false) +limit_repair_value: 39 + +# Whether the anvil's cost limit should be removed entirely. +# +# The anvil will still visually display "Too Expensive" if "replace_too_expensive" is disabled +# However, the action will be completable if xp requirement is meet. +remove_repair_limit: false + +# Whenever anvil cost is above 39 should display the true price and not "Too Expensive". +# +# However, when bypassing "Too Expensive", anvil price will be displayed as Green. +# If the action is not completable, the cost will still be displayed as "Too expensive". +# That mean you also need to change other settings like remove_repair_limit or limit_repair_cost. +# +# Require ProtocoLib. +replace_too_expensive: false + +# XP Level amount added to the anvil when the item is repaired by another item of the same type +# +# Valid values include 0 to 1000 +item_repair_cost: 2 + +# XP Level amount added to the anvil when the item is renamed +# +# Valid values include 0 to 1000 +item_rename_cost: 1 + +# XP Level amount added to the anvil when the item is repaired by an "unit" +# For example: a Diamond on a Diamond Sword +# What's considered unit for what can be edited on the unit repair configuration. +# +# Valid values include 0 to 1000 +unit_repair_cost: 1 + +# XP Level amount added to the anvil when a sacrifice enchantment +# conflict with one of the left item enchantment +# +# Valid values include 0 to 1000 +sacrifice_illegal_enchant_cost: 1 + +# Allow using color code and hexadecimal color. +# +# Color code are prefixed by "&" and hexadecimal color by "#". +# Color code will not be applied if it colors nothing. "&&" can be used to write "&". +# For minimessage search for minimessage formating https://docs.papermc.io/adventure/minimessage/format/ +allow_color_code: false +allow_hexadecimal_color: false +allow_minimessage: false + +# This enables restricting color code for player having specific permission +# It requires allow_color_code enabled for... obvious reasons +# +# For example: if player want to use "&aHello" it will be required that the player has +# the permission "ca.color.code.a" as he used the color code "a" +# In general permission to give to the player is "ca.color.code.[code]" +# where [code] is the color code you wish to allow the player +# +# It is kinda of useless when minimessage is supported as players would be able to bypass +# that using the equivalent minimessage tag +per_color_code_permission: false + +# Toggle if color should only be applicable if the player a certain permission. +# +# permission are "ca.color.code" for use of color code and "ca.color.hex" for use of hexadecimal color. +permission_needed_for_color: true + +# Xp cost if the player use color in the items name on rename. +# +# Valid values include 0 to 1000. +use_of_color_cost: 0 + +# Dialogue rename menu make use of dialog menu to allow bigger rename +# You can also change the maximum size and set it to -1 or less for maximum +# +# This feature only work on paper 1.21.7 or later +# +# At the moment only english is available for this menu... sorry ! +# +# CustomAnvil use "ca.rename.dialog" when permission +enable_dialog_rename: false +dialog_rename_max_size: 256 +permission_needed_for_dialog_rename: false + +# This allows custom anvil to not "guess" the text used for rename but store it in the item +# It will make item stackable only and only if it had used the same rename text +# +# For practical reason. this only work when dialog rename is enabled +dialog_rename_keep_user_text: true + +# Override limits for specific enchants +# +# Enchantments not listed here will use the value of default_limit +# +# Overrides provided default from aqua_affinity to depth_strider won't change effect with extra levels +# +# Valid range of 0 - 255 for each enchantment +# -1 mean keep default +enchant_limits: + minecraft:aqua_affinity: 1 + minecraft:binding_curse: 1 + minecraft:channeling: 1 + minecraft:flame: 1 + minecraft:infinity: 1 + minecraft:mending: 1 + minecraft:multishot: 1 + minecraft:silk_touch: 1 + minecraft:vanishing_curse: 1 + minecraft:depth_strider: 3 # anything more than 3 is treated as 3 by the game + minecraft:protection: 4 + minecraft:fire_protection: 4 + minecraft:blast_protection: 4 + minecraft:projectile_protection: 4 + minecraft:feather_falling: 4 + minecraft:thorns: 3 + minecraft:respiration: 3 + minecraft:sharpness: 5 + minecraft:smite: 5 + minecraft:bane_of_arthropods: 5 + minecraft:knockback: 2 + minecraft:fire_aspect: 2 + minecraft:looting: 3 + minecraft:sweeping: 3 + minecraft:sweeping_edge: 3 + minecraft:efficiency: 5 + minecraft:unbreaking: 3 + minecraft:fortune: 3 + minecraft:power: 5 + minecraft:punch: 2 + minecraft:luck_of_the_sea: 3 + minecraft:lure: 3 + minecraft:frost_walker: 2 + minecraft:impaling: 5 + minecraft:riptide: 3 + minecraft:loyalty: 3 + minecraft:piercing: 4 + minecraft:quick_charge: 3 + minecraft:soul_speed: 3 + minecraft:swift_sneak: 3 + minecraft:density: 5 + minecraft:breach: 4 + minecraft:wind_burst: 3 + +# Multipliers used to calculate the enchantment's value in repair/combining +# +# Values here are pulled from the fandom wiki: +# https://minecraft.fandom.com/wiki/Anvil_mechanics#Costs_for_combining_enchantments +# +# If an enchantment is missing values here, or is less than 0, it will default to 0 +# +# Calculated as: [Enchantment lvl] * [multiplier] +# +# With default values protection 4 would have a value of 4 when +# coming from either a book (4 * 1) or an item (4 * 1) +enchant_values: + minecraft:aqua_affinity: + item: 4 + book: 2 + minecraft:bane_of_arthropods: + item: 2 + book: 1 + minecraft:binding_curse: + item: 8 + book: 4 + minecraft:blast_protection: + item: 4 + book: 2 + minecraft:channeling: + item: 8 + book: 4 + minecraft:depth_strider: + item: 4 + book: 2 + minecraft:efficiency: + item: 1 + book: 1 + minecraft:flame: + item: 4 + book: 2 + minecraft:feather_falling: + item: 2 + book: 1 + minecraft:fire_aspect: + item: 4 + book: 2 + minecraft:fire_protection: + item: 2 + book: 1 + minecraft:fortune: + item: 4 + book: 2 + minecraft:frost_walker: + item: 4 + book: 2 + minecraft:impaling: + item: 4 + book: 2 + minecraft:infinity: + item: 8 + book: 4 + minecraft:knockback: + item: 2 + book: 1 + minecraft:looting: + item: 4 + book: 2 + minecraft:loyalty: + item: 1 + book: 1 + minecraft:luck_of_the_sea: + item: 4 + book: 2 + minecraft:lure: + item: 4 + book: 2 + minecraft:mending: + item: 4 + book: 2 + minecraft:multishot: + item: 4 + book: 2 + minecraft:piercing: + item: 1 + book: 1 + minecraft:power: + item: 1 + book: 1 + minecraft:projectile_protection: + item: 2 + book: 1 + minecraft:protection: + item: 1 + book: 1 + minecraft:punch: + item: 4 + book: 2 + minecraft:quick_charge: + item: 2 + book: 1 + minecraft:respiration: + item: 4 + book: 2 + minecraft:riptide: + item: 4 + book: 2 + minecraft:silk_touch: + item: 8 + book: 4 + minecraft:sharpness: + item: 1 + book: 1 + minecraft:smite: + item: 2 + book: 1 + minecraft:soul_speed: + item: 8 + book: 4 + minecraft:swift_sneak: + item: 8 + book: 4 + minecraft:sweeping: + item: 4 + book: 2 + minecraft:sweeping_edge: + item: 4 + book: 2 + minecraft:thorns: + item: 8 + book: 4 + minecraft:unbreaking: + item: 2 + book: 1 + minecraft:vanishing_curse: + item: 8 + book: 4 + minecraft:density: + item: 2 + book: 1 + minecraft:breach: + item: 4 + book: 2 + minecraft:wind_burst: + item: 4 + book: 2 + +# Disable enchantment merging for level above the set value +# Enchantment merging is when, for example, 2 unbreaking II book combine to give sharpness III +# But Enchantment above this value can still be applied. following the previous example, we could still apply a unbreaking III book to a sword +# Even if disable-merge-over of unbreaking is set to 2 +# -1 mean enchantment merge for this enchantment is not disabled. default to -1 if absent. +disable-merge-over: + # Sharpness is set to -1. it equivalent to it not being set to anything (and work as vanilla on default configuration) + minecraft:sharpness: -1 +# If uncommented. 2 unbreaking II book would not give an unbreaking III book. but unbreaking III book can still be applied +# minecraft:unbreaking: 2 + +# The maximum number of enchantment an item can get. -1 for infinity +# Use eco enchant enchant_limit if present by default unless "default" is not equal to -1 +enchantment_count_limit: + default: -1 + # Limit for specific items. example bellow is an example with stick + # Per item enchantment limit override eco enchant enchant_limit and default limit + items: + stick: -1 + +# Settings for lore modification +lore_edit: + book_and_quil: + # Permission is ca.lore_edit.book + use_permission: true + append: + # If adding lore using book & quil is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Cost used for every lore line added + per_line_cost: 0 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If adding the lore consume the book & quil + do_consume: false + # Allow using color code and hexadecimal color when editing lore via book & quil + # + # Color code are prefixed by "&" and hexadecimal color by "#" + # Color code will not be applied if it colors nothing. "&&" can be used to write "&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + # + # Note that currently minimessage would disable hex code when adding color + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + + remove: + # If removing lore using book & quil is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Cost used for every lore line removed + per_line_cost: 0 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If removing the lore consume the book & quil + do_consume: false + # Cost of replacing colors + remove_color_cost: 0 + # Allowed some color and tags to be reverted to plain text + # Custom anvil will prioritise format that result is a smaller resulting text + # Note that not allowing certain format will lead to some lost of color or tags. + # If configuration are exact as append appending this book should result in the exact same color + # + # Color code will be prefixed by "&" and hexadecimal color by "#". + # If color code is allowed, "&" in the text will get converted to "&&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + + paper: + # Permission is ca.lore_edit.paper + use_permission: true + # what order should the lines should get added/removed (start/end, if invalid or not present will be end) + order: end + + append_line: + # If adding lore line using paper is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If adding the lore line consume the paper + do_consume: false + # Allow using color code and hexadecimal color when editing lore via book & quil + # + # Color code are prefixed by "&" and hexadecimal color by "#" + # Color code will not be applied if it colors nothing. "&&" can be used to write "&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + # + # Note that currently minimessage would disable hex code when adding color + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + color_use_cost: 0 + + remove_line: + # If removing lore line using paper is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If removing the lore line consume the paper + do_consume: false + # Cost of replacing colors + remove_color_cost: 0 + # Allowed some color and tags to be reverted to plain text + # Custom anvil will prioritise format that result is a smaller resulting text + # Note that not allowing certain format will lead to some lost of color or tags. + # If configuration are exact as append appending this paper should result in the exact same color + # + # Color code will be prefixed by "&" and hexadecimal color by "#". + # If color code is allowed, "&" in the text will get converted to "&&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + +# Allow to replace the xp cost by a monetary cost +# If enabled it will not be bound to the experience level limits +# +# It also requires to enable dialog rename (set "enable_dialog_rename: false" a bit higher) +# If dialog rename permission is enabled and player do not have the permission merge will fall back to vanilla xp cost +# +# If you are using custom craft I recommend using Linear Xp Cost with Exact Linear Xp as normal Xp Cost will act "weird" +# But Linear Xp will act as 1$ time global multiplier. In other word: like you expect +# +# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 and later +monetary_cost: + enabled: false + # If using vault unlocked this allow to specify what currency should be used for anvil usage + # default being the default currency + currency: default + # multiply the anvil cost by a value to allow to have price a big bigger than like 40 + multipliers: + # global multipliers. all usage type will be multiplied by this value + global: 1.0 + # usage specific type. it will only apply for specific xp "reason" + enchantment: 1.0 # related to enchantments level + repair: 1.0 # for repairing via unit repair (per unit) + rename: 1.0 # for renaming the item + lore_edit: 1.0 # for changing the lore of the item (only if lore edit is enabled) + work_penalty: 1.0 # for work penalty (aka use penalty) + recipe: 1.0 # for custom anvil recipe cost + +# Whether to show debug logging +debug_log: false + +# Whether to show verbose debug logging +debug_log_verbose: false + +configVersion: 1.11.0 +lowMinecraftVersion: 1.21.9 diff --git a/defaultconfigs/1.21.9/custom_recipes.yml b/defaultconfigs/1.21.9/custom_recipes.yml new file mode 100644 index 0000000..57c2220 --- /dev/null +++ b/defaultconfigs/1.21.9/custom_recipes.yml @@ -0,0 +1,5 @@ +# ---------------------------------------------------- +# This config file is to store custom craft +# It is recommended to use the in game config editor for this configuration. +# /customanvilconfig With ca.config.edit permission +# ---------------------------------------------------- diff --git a/defaultconfigs/1.21.9/enchant_conflict.yml b/defaultconfigs/1.21.9/enchant_conflict.yml new file mode 100644 index 0000000..9205061 --- /dev/null +++ b/defaultconfigs/1.21.9/enchant_conflict.yml @@ -0,0 +1,389 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# material conflicts +# +# If you want to edit this file: +# - A conflict will apply to every item except if in one of the notAffectedGroups group +# - the conflict will count only if the user try to combine at least as +# many conflicting enchantment as "maxEnchantmentBeforeConflict" +# +# +# ---------------------------------------------------- +# These restriction are about not allowing enchantment +# on illegal items +# ---------------------------------------------------- + +restriction_aqua_affinity: + enchantments: + - minecraft:aqua_affinity + notAffectedGroups: + - enchanted_book + - helmets + +restriction_bane_of_arthropods: + enchantments: + - minecraft:bane_of_arthropods + notAffectedGroups: + - enchanted_book + - melee_weapons + - mace + +restriction_blast_protection: + enchantments: + - minecraft:blast_protection + notAffectedGroups: + - enchanted_book + - armors + +restriction_channeling: + enchantments: + - minecraft:channeling + notAffectedGroups: + - enchanted_book + - trident + +restriction_binding_curse: + enchantments: + - minecraft:binding_curse + notAffectedGroups: + - enchanted_book + - wearable + +restriction_vanishing_curse: + enchantments: + - minecraft:vanishing_curse + notAffectedGroups: + - enchanted_book + - can_vanish + +restriction_depth_strider: + enchantments: + - minecraft:depth_strider + notAffectedGroups: + - enchanted_book + - boots + +restriction_efficiency: + enchantments: + - minecraft:efficiency + notAffectedGroups: + - enchanted_book + - tools + - shears + +restriction_feather_falling: + enchantments: + - minecraft:feather_falling + notAffectedGroups: + - enchanted_book + - boots + +restriction_fire_aspect: + enchantments: + - minecraft:fire_aspect + notAffectedGroups: + - enchanted_book + - swords + - mace + +restriction_fire_protection: + enchantments: + - minecraft:fire_protection + notAffectedGroups: + - enchanted_book + - armors + +restriction_flame: + enchantments: + - minecraft:flame + notAffectedGroups: + - enchanted_book + - bow + +restriction_fortune: + enchantments: + - minecraft:fortune + notAffectedGroups: + - enchanted_book + - tools + +restriction_frost_walker: + enchantments: + - minecraft:frost_walker + notAffectedGroups: + - enchanted_book + - boots + +restriction_impaling: + enchantments: + - minecraft:impaling + notAffectedGroups: + - enchanted_book + - trident + +restriction_infinity: + enchantments: + - minecraft:infinity + notAffectedGroups: + - enchanted_book + - bow + +restriction_knockback: + enchantments: + - minecraft:knockback + notAffectedGroups: + - enchanted_book + - swords + +restriction_looting: + enchantments: + - minecraft:looting + notAffectedGroups: + - enchanted_book + - swords + +restriction_loyalty: + enchantments: + - minecraft:loyalty + notAffectedGroups: + - enchanted_book + - trident + +restriction_luck_of_the_sea: + enchantments: + - minecraft:luck_of_the_sea + notAffectedGroups: + - enchanted_book + - fishing_rod + +restriction_lure: + enchantments: + - minecraft:lure + notAffectedGroups: + - enchanted_book + - fishing_rod + +restriction_mending: + enchantments: + - minecraft:mending + notAffectedGroups: + - enchanted_book + - can_unbreak + +restriction_minecraft_multishot: + enchantments: + - minecraft:multishot + notAffectedGroups: + - enchanted_book + - crossbow + +restriction_piercing: + enchantments: + - minecraft:piercing + notAffectedGroups: + - enchanted_book + - crossbow + +restriction_power: + enchantments: + - minecraft:power + notAffectedGroups: + - enchanted_book + - bow + +restriction_projectile_protection: + enchantments: + - minecraft:projectile_protection + notAffectedGroups: + - enchanted_book + - armors + +restriction_protection: + enchantments: + - minecraft:protection + notAffectedGroups: + - enchanted_book + - armors + +restriction_punch: + enchantments: + - minecraft:punch + notAffectedGroups: + - enchanted_book + - bow + +restriction_quick_charge: + enchantments: + - minecraft:quick_charge + notAffectedGroups: + - enchanted_book + - crossbow + +restriction_respiration: + enchantments: + - minecraft:respiration + notAffectedGroups: + - enchanted_book + - helmets + +restriction_riptide: + enchantments: + - minecraft:riptide + notAffectedGroups: + - enchanted_book + - trident + +restriction_sharpness: + enchantments: + - minecraft:sharpness + notAffectedGroups: + - enchanted_book + - melee_weapons + +restriction__silk_touch: + enchantments: + - minecraft:silk_touch + notAffectedGroups: + - enchanted_book + - tools + +restriction_smite: + enchantments: + - minecraft:smite + notAffectedGroups: + - enchanted_book + - melee_weapons + - mace + +restriction_soul_speed: + enchantments: + - minecraft:soul_speed + notAffectedGroups: + - enchanted_book + - boots + +restriction_sweeping_edge: + enchantments: + - minecraft:sweeping + - minecraft:sweeping_edge + notAffectedGroups: + - enchanted_book + - swords + +# Do not exist in 1.18, that mean useInFuture will be set to true +# useInFuture set to true also mean it will not warn if there is an issue +restriction_swift_sneak: + useInFuture: true + enchantments: + - minecraft:swift_sneak + notAffectedGroups: + - enchanted_book + - leggings + +restriction_thorns: + enchantments: + - minecraft:thorns + notAffectedGroups: + - enchanted_book + - armors + +restriction__unbreaking: + enchantments: + - minecraft:unbreaking + notAffectedGroups: + - enchanted_book + - can_unbreak + +# ---------------------------------------------------- +# Now we have conflicts about enchantment Incompatibility +# We just filtered what item enchantments can be applied +# notAffectedGroups is empty as we don't want anything to not respect theses rules +# maxEnchantmentBeforeConflict is set to 1 to only have 1 on those enchantment available +# ---------------------------------------------------- + +sword_enchant_conflict: + enchantments: + - minecraft:bane_of_arthropods + - minecraft:smite + - minecraft:sharpness + - minecraft:density + - minecraft:breach + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 + +protection_enchant_conflict: + enchantments: + - minecraft:blast_protection + - minecraft:fire_protection + - minecraft:projectile_protection + - minecraft:protection + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 + +trident_conflict1: + enchantments: + - minecraft:channeling + - minecraft:riptide + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 + +trident_conflict2: + enchantments: + - minecraft:loyalty + - minecraft:riptide + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 + +boot_conflict: + enchantments: + - minecraft:depth_strider + - minecraft:frost_walker + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 + +tool_conflict: + enchantments: + - minecraft:fortune + - minecraft:silk_touch + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 + +bow_conflict: + enchantments: + - minecraft:mending + - minecraft:infinity + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 + +crossbow_conflict: + enchantments: + - minecraft:multishot + - minecraft:piercing + notAffectedGroups: [] + maxEnchantmentBeforeConflict: 1 +restriction_density: + enchantments: + - minecraft:density + notAffectedGroups: + - mace + - enchanted_book +restriction_breach: + enchantments: + - minecraft:breach + notAffectedGroups: + - mace + - enchanted_book +restriction_wind_burst: + enchantments: + - minecraft:wind_burst + notAffectedGroups: + - mace + - enchanted_book + +# ---------------------------------------------------- +# Bellow is for custom conflicts. +# This is also where conflict create from the gui will be placed. +# ---------------------------------------------------- diff --git a/defaultconfigs/1.21.9/item_groups.yml b/defaultconfigs/1.21.9/item_groups.yml new file mode 100644 index 0000000..2dbd5db --- /dev/null +++ b/defaultconfigs/1.21.9/item_groups.yml @@ -0,0 +1,236 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# Please note this config use spigot material names. +# It should match minecraft name in most case, maybe every case, but I can't be sure +# In case there an issue with material name, you can found them here: +# https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html + +# An empty Exclude group exclude nothing, so it contain everything +everything: + type: exclude + +# An empty include group will include nothing +nothing: + type: include + +# This group is an example of a group including only stone and polished granite +example_include: + type: include + items: + - stone + - polished_granite + +# This group contain everything except polished granite and elements of example_include +example_exclude: + type: exclude + items: + - polished_granite + groups: + - example_include + +# Default configuration should be vanilla enchantment conflict group +# there may have error, if you find one you can fix it ! +# https://minecraft.fandom.com/wiki/Enchanting + +swords: + type: include + items: + - wooden_sword + - stone_sword + - iron_sword + - diamond_sword + - golden_sword + - netherite_sword + - copper_sword + +axes: + type: include + items: + - wooden_axe + - stone_axe + - iron_axe + - diamond_axe + - golden_axe + - netherite_axe + - copper_axe + +melee_weapons: + type: include + groups: + - swords + - axes + +helmets: + type: include + items: + - leather_helmet + - chainmail_helmet + - iron_helmet + - diamond_helmet + - golden_helmet + - netherite_helmet + - turtle_helmet + - copper_helmet + +chestplate: + type: include + items: + - leather_chestplate + - chainmail_chestplate + - iron_chestplate + - diamond_chestplate + - golden_chestplate + - netherite_chestplate + - copper_chestplate + +leggings: + type: include + items: + - leather_leggings + - chainmail_leggings + - iron_leggings + - diamond_leggings + - golden_leggings + - netherite_leggings + - copper_leggings + +boots: + type: include + items: + - leather_boots + - chainmail_boots + - iron_boots + - diamond_boots + - golden_boots + - netherite_boots + - copper_boots + +armors: + type: include + groups: + - helmets + - chestplate + - leggings + - boots + +wearable: + type: include + items: + - elytra + - carved_pumpkin + - skeleton_skull + - wither_skeleton_skull + - zombie_head + - player_head + - creeper_head + - dragon_head + - piglin_head + groups: + - armors + +pickaxes: + type: include + items: + - wooden_pickaxe + - stone_pickaxe + - iron_pickaxe + - diamond_pickaxe + - golden_pickaxe + - netherite_pickaxe + - copper_pickaxe + +shovels: + type: include + items: + - wooden_shovel + - stone_shovel + - iron_shovel + - diamond_shovel + - golden_shovel + - netherite_shovel + - copper_shovel + +hoes: + type: include + items: + - wooden_hoe + - stone_hoe + - iron_hoe + - diamond_hoe + - golden_hoe + - netherite_hoe + - copper_hoe + +tools: + type: include + groups: + - pickaxes + - shovels + - hoes + - axes + +enchanted_book: + type: include + items: + - enchanted_book + +trident: + type: include + items: + - trident + +bow: + type: include + items: + - bow + +crossbow: + type: include + items: + - crossbow + +fishing_rod: + type: include + items: + - fishing_rod + +shears: + type: include + items: + - shears + +can_unbreak: + type: include + items: + - elytra + - flint_and_steel + - shield + - carrot_on_a_stick + - warped_fungus_on_a_stick + - brush + groups: + - melee_weapons + - tools + - armors + - trident + - bow + - crossbow + - fishing_rod + - shears + - mace + +can_vanish: + type: include + items: + - compass + groups: + - wearable + - can_unbreak +mace: + type: include + items: + - mace + diff --git a/defaultconfigs/1.21.9/unit_repair_item.yml b/defaultconfigs/1.21.9/unit_repair_item.yml new file mode 100644 index 0000000..0ce2bce --- /dev/null +++ b/defaultconfigs/1.21.9/unit_repair_item.yml @@ -0,0 +1,192 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# Unit repair configuration +# +# This configuration is to make custom unit repair +# A unit repair is, for example, a diamond to repair a diamond sword +# In vanilla, a unit repair 25% of object durability +# you can make a custom value here +# +# Item name should NOT combine caps and no caps (example: Stone) + +# Default value if the config is an invalid value (value <= 0 ) +# If value > 1 it will be treated as being = 1 +default_repair_amount: 0.25 + +# You can add custom unit repair +# The example bellow make a shield repaired by 10% by sticks + +# stick: +# shield: 0.10 + + +# Vanilla unit repair group is bellow +diamond: + diamond_helmet: 0.25 + diamond_chestplate: 0.25 + diamond_leggings: 0.25 + diamond_boots: 0.25 + diamond_sword: 0.25 + diamond_pickaxe: 0.25 + diamond_axe: 0.25 + diamond_shovel: 0.25 + diamond_hoe: 0.25 + +netherite_ingot: + netherite_helmet: 0.25 + netherite_chestplate: 0.25 + netherite_leggings: 0.25 + netherite_boots: 0.25 + netherite_sword: 0.25 + netherite_pickaxe: 0.25 + netherite_axe: 0.25 + netherite_shovel: 0.25 + netherite_hoe: 0.25 + +gold_ingot: + golden_helmet: 0.25 + golden_chestplate: 0.25 + golden_leggings: 0.25 + golden_boots: 0.25 + golden_sword: 0.25 + golden_pickaxe: 0.25 + golden_axe: 0.25 + golden_shovel: 0.25 + golden_hoe: 0.25 + +iron_ingot: + iron_helmet: 0.25 + iron_chestplate: 0.25 + iron_leggings: 0.25 + iron_boots: 0.25 + iron_sword: 0.25 + iron_pickaxe: 0.25 + iron_axe: 0.25 + iron_shovel: 0.25 + iron_hoe: 0.25 + +cobblestone: + stone_sword: 0.25 + stone_pickaxe: 0.25 + stone_axe: 0.25 + stone_shovel: 0.25 + stone_hoe: 0.25 + +cobbled_deepslate: + stone_sword: 0.25 + stone_pickaxe: 0.25 + stone_axe: 0.25 + stone_shovel: 0.25 + stone_hoe: 0.25 + +blackstone: + stone_sword: 0.25 + stone_pickaxe: 0.25 + stone_axe: 0.25 + stone_shovel: 0.25 + stone_hoe: 0.25 + +leather: + leather_helmet: 0.25 + leather_chestplate: 0.25 + leather_leggings: 0.25 + leather_boots: 0.25 + +phantom_membrane: + elytra: 0.25 + +scute: + turtle_helmet: 0.25 + +oak_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +spruce_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +birch_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +jungle_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +acacia_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +dark_oak_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +mangrove_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +cherry_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +bamboo_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +crimson_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +warped_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 +breeze_rod: + mace: 0.25 diff --git a/defaultconfigs/1.21/README.md b/defaultconfigs/1.21/README.md new file mode 100644 index 0000000..7105d8a --- /dev/null +++ b/defaultconfigs/1.21/README.md @@ -0,0 +1,6 @@ +### Default Plugin's Configurations For 1.21 +- [config.yml](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21/config.yml) +- [enchant_conflict.yml](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21/enchant_conflict.yml) +- [item_groups.yml](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21/item_groups.yml) +- [unit_repair_item.yml](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21/unit_repair_item.yml) +- [custom_recipes.yml](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21/custom_recipes.yml) diff --git a/defaultconfigs/1.21/config.yml b/defaultconfigs/1.21/config.yml new file mode 100644 index 0000000..5d59e5a --- /dev/null +++ b/defaultconfigs/1.21/config.yml @@ -0,0 +1,466 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# What service of metric should custom anvil use +# Custom anvil collect generic information like server minecraft version, type, etc... +# It can also collect error information if error is happening (currently faststats only) +# It can also be disabled +# Please refer to README for public metric link +# Possible options: auto, bstat, faststats, disabled (auto by default) +metric_type: auto + +# Allow to report errors made caused by this plugin (only for faststats) +# This allows me to fix potentials issue that I'm not aware of +# Accept true or false (true by default) +metric_collect_errors: true + +# All anvil cost will be capped to limit_repair_value if enabled. +# +# In other words: +# For any anvil cost greater than limit_repair_value, Cost will be set to limit_repair_value. +limit_repair_cost: false + +# Max cost value the Anvil can get to. +# +# Valid values include 0 to 1000. +# Cost will be displayed as "Too Expensive": +# - If Cost is above 39 +# - And replace_too_expensive is disabled (false) +limit_repair_value: 39 + +# Whether the anvil's cost limit should be removed entirely. +# +# The anvil will still visually display "Too Expensive" if "replace_too_expensive" is disabled +# However, the action will be completable if xp requirement is meet. +remove_repair_limit: false + +# Whenever anvil cost is above 39 should display the true price and not "Too Expensive". +# +# However, when bypassing "Too Expensive", anvil price will be displayed as Green. +# If the action is not completable, the cost will still be displayed as "Too expensive". +# That mean you also need to change other settings like remove_repair_limit or limit_repair_cost. +# +# Require ProtocoLib. +replace_too_expensive: false + +# XP Level amount added to the anvil when the item is repaired by another item of the same type +# +# Valid values include 0 to 1000 +item_repair_cost: 2 + +# XP Level amount added to the anvil when the item is renamed +# +# Valid values include 0 to 1000 +item_rename_cost: 1 + +# XP Level amount added to the anvil when the item is repaired by an "unit" +# For example: a Diamond on a Diamond Sword +# What's considered unit for what can be edited on the unit repair configuration. +# +# Valid values include 0 to 1000 +unit_repair_cost: 1 + +# XP Level amount added to the anvil when a sacrifice enchantment +# conflict with one of the left item enchantment +# +# Valid values include 0 to 1000 +sacrifice_illegal_enchant_cost: 1 + +# Allow using color code and hexadecimal color. +# +# Color code are prefixed by "&" and hexadecimal color by "#". +# Color code will not be applied if it colors nothing. "&&" can be used to write "&". +# For minimessage search for minimessage formating https://docs.papermc.io/adventure/minimessage/format/ +allow_color_code: false +allow_hexadecimal_color: false +allow_minimessage: false + +# This enables restricting color code for player having specific permission +# It requires allow_color_code enabled for... obvious reasons +# +# For example: if player want to use "&aHello" it will be required that the player has +# the permission "ca.color.code.a" as he used the color code "a" +# In general permission to give to the player is "ca.color.code.[code]" +# where [code] is the color code you wish to allow the player +# +# It is kinda of useless when minimessage is supported as players would be able to bypass +# that using the equivalent minimessage tag +per_color_code_permission: false + +# Toggle if color should only be applicable if the player a certain permission. +# +# permission are "ca.color.code" for use of color code and "ca.color.hex" for use of hexadecimal color. +permission_needed_for_color: true + +# Xp cost if the player use color in the items name on rename. +# +# Valid values include 0 to 1000. +use_of_color_cost: 0 + +# Dialogue rename menu make use of dialog menu to allow bigger rename +# You can also change the maximum size and set it to -1 or less for maximum +# +# This feature only work on paper 1.21.7 or later +# +# At the moment only english is available for this menu... sorry ! +# +# CustomAnvil use "ca.rename.dialog" when permission +enable_dialog_rename: false +dialog_rename_max_size: 256 +permission_needed_for_dialog_rename: false + +# This allows custom anvil to not "guess" the text used for rename but store it in the item +# It will make item stackable only and only if it had used the same rename text +# +# For practical reason. this only work when dialog rename is enabled +dialog_rename_keep_user_text: true + +# Override limits for specific enchants +# +# Enchantments not listed here will use the value of default_limit +# +# Overrides provided default from aqua_affinity to depth_strider won't change effect with extra levels +# +# Valid range of 0 - 255 for each enchantment +# -1 mean keep default +enchant_limits: + minecraft:aqua_affinity: 1 + minecraft:binding_curse: 1 + minecraft:channeling: 1 + minecraft:flame: 1 + minecraft:infinity: 1 + minecraft:mending: 1 + minecraft:multishot: 1 + minecraft:silk_touch: 1 + minecraft:vanishing_curse: 1 + minecraft:depth_strider: 3 # anything more than 3 is treated as 3 by the game + minecraft:protection: 4 + minecraft:fire_protection: 4 + minecraft:blast_protection: 4 + minecraft:projectile_protection: 4 + minecraft:feather_falling: 4 + minecraft:thorns: 3 + minecraft:respiration: 3 + minecraft:sharpness: 5 + minecraft:smite: 5 + minecraft:bane_of_arthropods: 5 + minecraft:knockback: 2 + minecraft:fire_aspect: 2 + minecraft:looting: 3 + minecraft:sweeping: 3 + minecraft:sweeping_edge: 3 + minecraft:efficiency: 5 + minecraft:unbreaking: 3 + minecraft:fortune: 3 + minecraft:power: 5 + minecraft:punch: 2 + minecraft:luck_of_the_sea: 3 + minecraft:lure: 3 + minecraft:frost_walker: 2 + minecraft:impaling: 5 + minecraft:riptide: 3 + minecraft:loyalty: 3 + minecraft:piercing: 4 + minecraft:quick_charge: 3 + minecraft:soul_speed: 3 + minecraft:swift_sneak: 3 + +# Multipliers used to calculate the enchantment's value in repair/combining +# +# Values here are pulled from the fandom wiki: +# https://minecraft.fandom.com/wiki/Anvil_mechanics#Costs_for_combining_enchantments +# +# If an enchantment is missing values here, or is less than 0, it will default to 0 +# +# Calculated as: [Enchantment lvl] * [multiplier] +# +# With default values protection 4 would have a value of 4 when +# coming from either a book (4 * 1) or an item (4 * 1) +enchant_values: + minecraft:aqua_affinity: + item: 4 + book: 2 + minecraft:bane_of_arthropods: + item: 2 + book: 1 + minecraft:binding_curse: + item: 8 + book: 4 + minecraft:blast_protection: + item: 4 + book: 2 + minecraft:channeling: + item: 8 + book: 4 + minecraft:depth_strider: + item: 4 + book: 2 + minecraft:efficiency: + item: 1 + book: 1 + minecraft:flame: + item: 4 + book: 2 + minecraft:feather_falling: + item: 2 + book: 1 + minecraft:fire_aspect: + item: 4 + book: 2 + minecraft:fire_protection: + item: 2 + book: 1 + minecraft:fortune: + item: 4 + book: 2 + minecraft:frost_walker: + item: 4 + book: 2 + minecraft:impaling: + item: 4 + book: 2 + minecraft:infinity: + item: 8 + book: 4 + minecraft:knockback: + item: 2 + book: 1 + minecraft:looting: + item: 4 + book: 2 + minecraft:loyalty: + item: 1 + book: 1 + minecraft:luck_of_the_sea: + item: 4 + book: 2 + minecraft:lure: + item: 4 + book: 2 + minecraft:mending: + item: 4 + book: 2 + minecraft:multishot: + item: 4 + book: 2 + minecraft:piercing: + item: 1 + book: 1 + minecraft:power: + item: 1 + book: 1 + minecraft:projectile_protection: + item: 2 + book: 1 + minecraft:protection: + item: 1 + book: 1 + minecraft:punch: + item: 4 + book: 2 + minecraft:quick_charge: + item: 2 + book: 1 + minecraft:respiration: + item: 4 + book: 2 + minecraft:riptide: + item: 4 + book: 2 + minecraft:silk_touch: + item: 8 + book: 4 + minecraft:sharpness: + item: 1 + book: 1 + minecraft:smite: + item: 2 + book: 1 + minecraft:soul_speed: + item: 8 + book: 4 + minecraft:swift_sneak: + item: 8 + book: 4 + minecraft:sweeping: + item: 4 + book: 2 + minecraft:sweeping_edge: + item: 4 + book: 2 + minecraft:thorns: + item: 8 + book: 4 + minecraft:unbreaking: + item: 2 + book: 1 + minecraft:vanishing_curse: + item: 8 + book: 4 + +# Disable enchantment merging for level above the set value +# Enchantment merging is when, for example, 2 unbreaking II book combine to give sharpness III +# But Enchantment above this value can still be applied. following the previous example, we could still apply a unbreaking III book to a sword +# Even if disable-merge-over of unbreaking is set to 2 +# -1 mean enchantment merge for this enchantment is not disabled. default to -1 if absent. +disable-merge-over: + # Sharpness is set to -1. it equivalent to it not being set to anything (and work as vanilla on default configuration) + minecraft:sharpness: -1 +# If uncommented. 2 unbreaking II book would not give an unbreaking III book. but unbreaking III book can still be applied +# minecraft:unbreaking: 2 + +# The maximum number of enchantment an item can get. -1 for infinity +# Use eco enchant enchant_limit if present by default unless "default" is not equal to -1 +enchantment_count_limit: + default: -1 + # Limit for specific items. example bellow is an example with stick + # Per item enchantment limit override eco enchant enchant_limit and default limit + items: + stick: -1 + +# Settings for lore modification +lore_edit: + book_and_quil: + # Permission is ca.lore_edit.book + use_permission: true + append: + # If adding lore using book & quil is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Cost used for every lore line added + per_line_cost: 0 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If adding the lore consume the book & quil + do_consume: false + # Allow using color code and hexadecimal color when editing lore via book & quil + # + # Color code are prefixed by "&" and hexadecimal color by "#" + # Color code will not be applied if it colors nothing. "&&" can be used to write "&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + # + # Note that currently minimessage would disable hex code when adding color + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + + remove: + # If removing lore using book & quil is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Cost used for every lore line removed + per_line_cost: 0 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If removing the lore consume the book & quil + do_consume: false + # Cost of replacing colors + remove_color_cost: 0 + # Allowed some color and tags to be reverted to plain text + # Custom anvil will prioritise format that result is a smaller resulting text + # Note that not allowing certain format will lead to some lost of color or tags. + # If configuration are exact as append appending this book should result in the exact same color + # + # Color code will be prefixed by "&" and hexadecimal color by "#". + # If color code is allowed, "&" in the text will get converted to "&&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + + paper: + # Permission is ca.lore_edit.paper + use_permission: true + # what order should the lines should get added/removed (start/end, if invalid or not present will be end) + order: end + + append_line: + # If adding lore line using paper is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If adding the lore line consume the paper + do_consume: false + # Allow using color code and hexadecimal color when editing lore via book & quil + # + # Color code are prefixed by "&" and hexadecimal color by "#" + # Color code will not be applied if it colors nothing. "&&" can be used to write "&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + # + # Note that currently minimessage would disable hex code when adding color + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + color_use_cost: 0 + + remove_line: + # If removing lore line using paper is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If removing the lore line consume the paper + do_consume: false + # Cost of replacing colors + remove_color_cost: 0 + # Allowed some color and tags to be reverted to plain text + # Custom anvil will prioritise format that result is a smaller resulting text + # Note that not allowing certain format will lead to some lost of color or tags. + # If configuration are exact as append appending this paper should result in the exact same color + # + # Color code will be prefixed by "&" and hexadecimal color by "#". + # If color code is allowed, "&" in the text will get converted to "&&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + +# Allow to replace the xp cost by a monetary cost +# If enabled it will not be bound to the experience level limits +# +# It also requires to enable dialog rename (set "enable_dialog_rename: false" a bit higher) +# If dialog rename permission is enabled and player do not have the permission merge will fall back to vanilla xp cost +# +# If you are using custom craft I recommend using Linear Xp Cost with Exact Linear Xp as normal Xp Cost will act "weird" +# But Linear Xp will act as 1$ time global multiplier. In other word: like you expect +# +# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 and later +monetary_cost: + enabled: false + # If using vault unlocked this allow to specify what currency should be used for anvil usage + # default being the default currency + currency: default + # multiply the anvil cost by a value to allow to have price a big bigger than like 40 + multipliers: + # global multipliers. all usage type will be multiplied by this value + global: 1.0 + # usage specific type. it will only apply for specific xp "reason" + enchantment: 1.0 # related to enchantments level + repair: 1.0 # for repairing via unit repair (per unit) + rename: 1.0 # for renaming the item + lore_edit: 1.0 # for changing the lore of the item (only if lore edit is enabled) + work_penalty: 1.0 # for work penalty (aka use penalty) + recipe: 1.0 # for custom anvil recipe cost + +# Whether to show debug logging +debug_log: false + +# Whether to show verbose debug logging +debug_log_verbose: false + +configVersion: 1.11.0 \ No newline at end of file diff --git a/defaultconfigs/1.21/custom_recipes.yml b/defaultconfigs/1.21/custom_recipes.yml new file mode 100644 index 0000000..57c2220 --- /dev/null +++ b/defaultconfigs/1.21/custom_recipes.yml @@ -0,0 +1,5 @@ +# ---------------------------------------------------- +# This config file is to store custom craft +# It is recommended to use the in game config editor for this configuration. +# /customanvilconfig With ca.config.edit permission +# ---------------------------------------------------- diff --git a/defaultconfigs/1.21/enchant_conflict.yml b/defaultconfigs/1.21/enchant_conflict.yml new file mode 100644 index 0000000..45d62c3 --- /dev/null +++ b/defaultconfigs/1.21/enchant_conflict.yml @@ -0,0 +1,247 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# material conflicts +# +# If you want to edit this file: +# - A conflict will apply to every item except if in one of the notAffectedGroups group +# - the conflict will count only if the user try to combine at least as +# many conflicting enchantment as "maxEnchantmentBeforeConflict" +# +# +# ---------------------------------------------------- +# These restriction are about not allowing enchantment +# on illegal items +# ---------------------------------------------------- + +restriction_aqua_affinity: + enchantments: [ minecraft:aqua_affinity ] + notAffectedGroups: [ enchanted_book, helmets ] + +restriction_bane_of_arthropods: + enchantments: [ minecraft:bane_of_arthropods ] + notAffectedGroups: [ enchanted_book, melee_weapons ] + +restriction_blast_protection: + enchantments: [ minecraft:blast_protection ] + notAffectedGroups: [ enchanted_book, armors ] + +restriction_channeling: + enchantments: [ minecraft:channeling ] + notAffectedGroups: [ enchanted_book, trident ] + +restriction_binding_curse: + enchantments: [ minecraft:binding_curse ] + notAffectedGroups: [ enchanted_book, wearable ] + +restriction_vanishing_curse: + enchantments: [ minecraft:vanishing_curse ] + notAffectedGroups: [ enchanted_book, can_vanish ] + +restriction_depth_strider: + enchantments: [ minecraft:depth_strider ] + notAffectedGroups: [ enchanted_book, boots ] + +restriction_efficiency: + enchantments: [ minecraft:efficiency ] + notAffectedGroups: [ enchanted_book, tools, shears ] + +restriction_feather_falling: + enchantments: [ minecraft:feather_falling ] + notAffectedGroups: [ enchanted_book, boots ] + +restriction_fire_aspect: + enchantments: [ minecraft:fire_aspect ] + notAffectedGroups: [ enchanted_book, swords ] + +restriction_fire_protection: + enchantments: [ minecraft:fire_protection ] + notAffectedGroups: [ enchanted_book, armors ] + +restriction_flame: + enchantments: [ minecraft:flame ] + notAffectedGroups: [ enchanted_book, bow ] + +restriction_fortune: + enchantments: [ minecraft:fortune ] + notAffectedGroups: [ enchanted_book, tools ] + +restriction_frost_walker: + enchantments: [ minecraft:frost_walker ] + notAffectedGroups: [ enchanted_book, boots ] + +restriction_impaling: + enchantments: [ minecraft:impaling ] + notAffectedGroups: [ enchanted_book, trident ] + +restriction_infinity: + enchantments: [ minecraft:infinity ] + notAffectedGroups: [ enchanted_book, bow ] + +restriction_knockback: + enchantments: [ minecraft:knockback ] + notAffectedGroups: [ enchanted_book, swords ] + +restriction_looting: + enchantments: [ minecraft:looting ] + notAffectedGroups: [ enchanted_book, swords ] + +restriction_loyalty: + enchantments: [ minecraft:loyalty ] + notAffectedGroups: [ enchanted_book, trident ] + +restriction_luck_of_the_sea: + enchantments: [ minecraft:luck_of_the_sea ] + notAffectedGroups: [ enchanted_book, fishing_rod ] + +restriction_lure: + enchantments: [ minecraft:lure ] + notAffectedGroups: [ enchanted_book, fishing_rod ] + +restriction_mending: + enchantments: [ minecraft:mending ] + notAffectedGroups: [ enchanted_book, can_unbreak ] + +restriction_minecraft_multishot: + enchantments: [ minecraft:multishot ] + notAffectedGroups: [ enchanted_book, crossbow ] + +restriction_piercing: + enchantments: [ minecraft:piercing ] + notAffectedGroups: [ enchanted_book, crossbow ] + +restriction_power: + enchantments: [ minecraft:power ] + notAffectedGroups: [ enchanted_book, bow ] + +restriction_projectile_protection: + enchantments: [ minecraft:projectile_protection ] + notAffectedGroups: [ enchanted_book, armors ] + +restriction_protection: + enchantments: [ minecraft:protection ] + notAffectedGroups: [ enchanted_book, armors ] + +restriction_punch: + enchantments: [ minecraft:punch ] + notAffectedGroups: [ enchanted_book, bow ] + +restriction_quick_charge: + enchantments: [ minecraft:quick_charge ] + notAffectedGroups: [ enchanted_book, crossbow ] + +restriction_respiration: + enchantments: [ minecraft:respiration ] + notAffectedGroups: [ enchanted_book, helmets ] + +restriction_riptide: + enchantments: [ minecraft:riptide ] + notAffectedGroups: [ enchanted_book, trident ] + +restriction_sharpness: + enchantments: [ minecraft:sharpness ] + notAffectedGroups: [ enchanted_book, melee_weapons ] + +restriction__silk_touch: + enchantments: [ minecraft:silk_touch ] + notAffectedGroups: [ enchanted_book, tools ] + +restriction_smite: + enchantments: [ minecraft:smite ] + notAffectedGroups: [ enchanted_book, melee_weapons ] + +restriction_soul_speed: + enchantments: [ minecraft:soul_speed ] + notAffectedGroups: [ enchanted_book, boots ] + +restriction_sweeping_edge: + enchantments: [ minecraft:sweeping, minecraft:sweeping_edge ] + notAffectedGroups: [ enchanted_book, swords ] + +# Do not exist in 1.18, that mean useInFuture will be set to true +# useInFuture set to true also mean it will not warn if there is an issue +restriction_swift_sneak: + useInFuture: true + enchantments: [ minecraft:swift_sneak ] + notAffectedGroups: [ enchanted_book, leggings ] + +restriction_thorns: + enchantments: [ minecraft:thorns ] + notAffectedGroups: [ enchanted_book, armors ] + +restriction__unbreaking: + enchantments: [ minecraft:unbreaking ] + notAffectedGroups: [ enchanted_book, can_unbreak ] + +# ---------------------------------------------------- +# Now we have conflicts about enchantment Incompatibility +# We just filtered what item enchantments can be applied +# notAffectedGroups is empty as we don't want anything to not respect theses rules +# maxEnchantmentBeforeConflict is set to 1 to only have 1 on those enchantment available +# ---------------------------------------------------- + +sword_enchant_conflict: + enchantments: + - minecraft:bane_of_arthropods + - minecraft:smite + - minecraft:sharpness + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +protection_enchant_conflict: + enchantments: + - minecraft:blast_protection + - minecraft:fire_protection + - minecraft:projectile_protection + - minecraft:protection + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +trident_conflict1: + enchantments: + - minecraft:channeling + - minecraft:riptide + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +trident_conflict2: + enchantments: + - minecraft:loyalty + - minecraft:riptide + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +boot_conflict: + enchantments: + - minecraft:depth_strider + - minecraft:frost_walker + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +tool_conflict: + enchantments: + - minecraft:fortune + - minecraft:silk_touch + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +bow_conflict: + enchantments: + - minecraft:mending + - minecraft:infinity + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +crossbow_conflict: + enchantments: + - minecraft:multishot + - minecraft:piercing + notAffectedGroups: [ ] + maxEnchantmentBeforeConflict: 1 + +# ---------------------------------------------------- +# Bellow is for custom conflicts. +# This is also where conflict create from the gui will be placed. +# ---------------------------------------------------- diff --git a/defaultconfigs/1.21/item_groups.yml b/defaultconfigs/1.21/item_groups.yml new file mode 100644 index 0000000..3c1eb5d --- /dev/null +++ b/defaultconfigs/1.21/item_groups.yml @@ -0,0 +1,210 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# Please note this config use spigot material names. +# It should match minecraft name in most case, maybe every case, but I can't be sure +# In case there an issue with material name, you can found them here: +# https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html + +# An empty Exclude group exclude nothing, so it contain everything +everything: + type: exclude + +# An empty include group will include nothing +nothing: + type: include + +# This group is an example of a group including only stone and polished granite +example_include: + type: include + items: + - stone + - polished_granite + +# This group contain everything except polished granite and elements of example_include +example_exclude: + type: exclude + items: + - polished_granite + groups: + - example_include + +# Default configuration should be vanilla enchantment conflict group +# there may have error, if you find one you can fix it ! +# https://minecraft.fandom.com/wiki/Enchanting + +swords: + type: include + items: + - wooden_sword + - stone_sword + - iron_sword + - diamond_sword + - golden_sword + - netherite_sword + +axes: + type: include + items: + - wooden_axe + - stone_axe + - iron_axe + - diamond_axe + - golden_axe + - netherite_axe + +melee_weapons: + type: include + groups: + - swords + - axes + +helmets: + type: include + items: + - leather_helmet + - chainmail_helmet + - iron_helmet + - diamond_helmet + - golden_helmet + - netherite_helmet + - turtle_helmet + +chestplate: + type: include + items: + - leather_chestplate + - chainmail_chestplate + - iron_chestplate + - diamond_chestplate + - golden_chestplate + - netherite_chestplate + +leggings: + type: include + items: + - leather_leggings + - chainmail_leggings + - iron_leggings + - diamond_leggings + - golden_leggings + - netherite_leggings + +boots: + type: include + items: + - leather_boots + - chainmail_boots + - iron_boots + - diamond_boots + - golden_boots + - netherite_boots + +armors: + type: include + groups: + - helmets + - chestplate + - leggings + - boots + +wearable: + type: include + items: + - elytra + - carved_pumpkin + - skeleton_skull + - wither_skeleton_skull + - zombie_head + - player_head + - creeper_head + - dragon_head + # do not exist in 1.18 but exist in future update + - piglin_head + groups: + - armors + +tools: + type: include + items: + - wooden_pickaxe + - stone_pickaxe + - iron_pickaxe + - diamond_pickaxe + - golden_pickaxe + - netherite_pickaxe + - wooden_shovel + - stone_shovel + - iron_shovel + - diamond_shovel + - golden_shovel + - netherite_shovel + - wooden_hoe + - stone_hoe + - iron_hoe + - diamond_hoe + - golden_hoe + - netherite_hoe + groups: + - axes + +enchanted_book: + type: include + items: + - enchanted_book + +trident: + type: include + items: + - trident + +bow: + type: include + items: + - bow + +crossbow: + type: include + items: + - crossbow + +fishing_rod: + type: include + items: + - fishing_rod + +shears: + type: include + items: + - shears + +can_unbreak: + type: include + items: + - elytra + - flint_and_steel + - shield + - carrot_on_a_stick + - warped_fungus_on_a_stick + # do not exist in 1.18 but exist in future update + - brush + groups: + - melee_weapons + - tools + - armors + - trident + - bow + - crossbow + - fishing_rod + - shears + +can_vanish: + type: include + items: + - compass + groups: + - wearable + - can_unbreak + diff --git a/defaultconfigs/1.21/unit_repair_item.yml b/defaultconfigs/1.21/unit_repair_item.yml new file mode 100644 index 0000000..2902cce --- /dev/null +++ b/defaultconfigs/1.21/unit_repair_item.yml @@ -0,0 +1,190 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# Unit repair configuration +# +# This configuration is to make custom unit repair +# A unit repair is, for example, a diamond to repair a diamond sword +# In vanilla, a unit repair 25% of object durability +# you can make a custom value here +# +# Item name should NOT combine caps and no caps (example: Stone) + +# Default value if the config is an invalid value (value <= 0 ) +# If value > 1 it will be treated as being = 1 +default_repair_amount: 0.25 + +# You can add custom unit repair +# The example bellow make a shield repaired by 10% by sticks + +#stick: +# shield: 0.10 + + +# Vanilla unit repair group is bellow +diamond: + diamond_helmet: 0.25 + diamond_chestplate: 0.25 + diamond_leggings: 0.25 + diamond_boots: 0.25 + diamond_sword: 0.25 + diamond_pickaxe: 0.25 + diamond_axe: 0.25 + diamond_shovel: 0.25 + diamond_hoe: 0.25 + +netherite_ingot: + netherite_helmet: 0.25 + netherite_chestplate: 0.25 + netherite_leggings: 0.25 + netherite_boots: 0.25 + netherite_sword: 0.25 + netherite_pickaxe: 0.25 + netherite_axe: 0.25 + netherite_shovel: 0.25 + netherite_hoe: 0.25 + +gold_ingot: + golden_helmet: 0.25 + golden_chestplate: 0.25 + golden_leggings: 0.25 + golden_boots: 0.25 + golden_sword: 0.25 + golden_pickaxe: 0.25 + golden_axe: 0.25 + golden_shovel: 0.25 + golden_hoe: 0.25 + +iron_ingot: + iron_helmet: 0.25 + iron_chestplate: 0.25 + iron_leggings: 0.25 + iron_boots: 0.25 + iron_sword: 0.25 + iron_pickaxe: 0.25 + iron_axe: 0.25 + iron_shovel: 0.25 + iron_hoe: 0.25 + +cobblestone: + stone_sword: 0.25 + stone_pickaxe: 0.25 + stone_axe: 0.25 + stone_shovel: 0.25 + stone_hoe: 0.25 + +cobbled_deepslate: + stone_sword: 0.25 + stone_pickaxe: 0.25 + stone_axe: 0.25 + stone_shovel: 0.25 + stone_hoe: 0.25 + +blackstone: + stone_sword: 0.25 + stone_pickaxe: 0.25 + stone_axe: 0.25 + stone_shovel: 0.25 + stone_hoe: 0.25 + +leather: + leather_helmet: 0.25 + leather_chestplate: 0.25 + leather_leggings: 0.25 + leather_boots: 0.25 + +phantom_membrane: + elytra: 0.25 + +scute: + turtle_helmet: 0.25 + +oak_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +spruce_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +birch_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +jungle_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +acacia_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +dark_oak_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +mangrove_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +cherry_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +bamboo_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +crimson_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +warped_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 diff --git a/defaultconfigs/README.md b/defaultconfigs/README.md new file mode 100644 index 0000000..daa2088 --- /dev/null +++ b/defaultconfigs/README.md @@ -0,0 +1,5 @@ +### Default Plugin's Configurations +From 1.18 to 1.20.6 use [1.18 configurations](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.18) \ +From 1.21 to 1.21.8 use [1.21 configurations](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21) \ +From 1.21.9 to 1.21.10 use [1.21.9 configurations](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21.9) \ +From 1.21.11 use [1.21.11 configurations](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21.11) diff --git a/gradle.properties b/gradle.properties index 29e08e8..4397d2e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,13 @@ -kotlin.code.style=official \ No newline at end of file +kotlin.code.style=official + +# Signing +signing.secretKeyRingFile=~/.gnupg/secring.gpg + +kotlin.daemon.jvmargs=-Xmx8G + +# list of nms +subprojects.reobfnms=v1_17R1,v1_18R1,v1_18R2,v1_19R1,v1_19R2,v1_19R3,v1_20R1,v1_20R2,v1_20R3,v1_20R4,v1_21R1,v1_21R2,v1_21R3,v1_21R4,v1_21R5,v1_21R6,v1_21R7 + +# list of version for hangar release +paperVersion=1.18-26.2 + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 69a9715..0b55a3b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/impl/ExcellentEnchant5_4/build.gradle.kts b/impl/ExcellentEnchant5_4/build.gradle.kts new file mode 100644 index 0000000..9fe598b --- /dev/null +++ b/impl/ExcellentEnchant5_4/build.gradle.kts @@ -0,0 +1,17 @@ +group = rootProject.group +version = rootProject.version + +plugins { + kotlin("jvm") version "2.3.0" +} + +repositories { + // ExcellentEnchants + maven(url = "https://repo.nightexpressdev.com/releases") +} + +dependencies { + // Excellent Enchant + compileOnly("su.nightexpress.excellentenchants:Core:5.4.3") + compileOnly("su.nightexpress.nightcore:main:2.16.2") +} \ No newline at end of file diff --git a/impl/ExcellentEnchant5_4/src/main/java/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchant5_3Registry.java b/impl/ExcellentEnchant5_4/src/main/java/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchant5_3Registry.java new file mode 100644 index 0000000..51e7302 --- /dev/null +++ b/impl/ExcellentEnchant5_4/src/main/java/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchant5_3Registry.java @@ -0,0 +1,16 @@ +package xyz.alexcrea.cuanvil.dependency.plugins; + +import org.jetbrains.annotations.NotNull; +import su.nightexpress.excellentenchants.api.enchantment.CustomEnchantment; +import su.nightexpress.excellentenchants.enchantment.EnchantRegistry; + +import java.util.Set; + +public class ExcellentEnchant5_3Registry { + + public static @NotNull Set getRegistered(){ + return EnchantRegistry.getRegistered(); + } + + +} diff --git a/impl/ExcellentEnchant5_4/src/main/java/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchant5_4EnchantSettings.java b/impl/ExcellentEnchant5_4/src/main/java/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchant5_4EnchantSettings.java new file mode 100644 index 0000000..49dde3e --- /dev/null +++ b/impl/ExcellentEnchant5_4/src/main/java/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchant5_4EnchantSettings.java @@ -0,0 +1,12 @@ +package xyz.alexcrea.cuanvil.dependency.plugins; + +import su.nightexpress.excellentenchants.EnchantsAPI; + +public class ExcellentEnchant5_4EnchantSettings { + + + public static int anvilLimit() { + return EnchantsAPI.getEnchantManager().getSettings().getAnvilEnchantsLimit(); + } + +} diff --git a/impl/LegacyEcoEnchant/.gitignore b/impl/LegacyEcoEnchant/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/impl/LegacyEcoEnchant/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/impl/LegacyEcoEnchant/build.gradle.kts b/impl/LegacyEcoEnchant/build.gradle.kts new file mode 100644 index 0000000..83057a7 --- /dev/null +++ b/impl/LegacyEcoEnchant/build.gradle.kts @@ -0,0 +1,13 @@ +group = rootProject.group +version = rootProject.version + +plugins { + kotlin("jvm") version "2.3.0" +} + +// Imitate needed class and method to support legacy version of EcoEnchant +dependencies { + // Spigot api + compileOnly("org.spigotmc:spigot-api:1.18-R0.1-SNAPSHOT") + +} \ No newline at end of file diff --git a/impl/LegacyEcoEnchant/src/main/java/com/willfp/ecoenchants/enchantments/EcoEnchant.java b/impl/LegacyEcoEnchant/src/main/java/com/willfp/ecoenchants/enchantments/EcoEnchant.java new file mode 100644 index 0000000..edfe1e9 --- /dev/null +++ b/impl/LegacyEcoEnchant/src/main/java/com/willfp/ecoenchants/enchantments/EcoEnchant.java @@ -0,0 +1,26 @@ +package com.willfp.ecoenchants.enchantments; + +import com.willfp.ecoenchants.enchantments.meta.EnchantmentTarget; +import com.willfp.ecoenchants.enchantments.meta.EnchantmentType; +import org.bukkit.enchantments.Enchantment; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +/** + * Mock class for legacy package of eco enchants + */ +public class EcoEnchant { + + public boolean conflictsWith(@NotNull Enchantment enchant) { + return false; + } + + public Set getTargets() { + return null; + } + + public EnchantmentType getType() { + return null; + } +} diff --git a/impl/LegacyEcoEnchant/src/main/java/com/willfp/ecoenchants/enchantments/EcoEnchants.java b/impl/LegacyEcoEnchant/src/main/java/com/willfp/ecoenchants/enchantments/EcoEnchants.java new file mode 100644 index 0000000..39f8064 --- /dev/null +++ b/impl/LegacyEcoEnchant/src/main/java/com/willfp/ecoenchants/enchantments/EcoEnchants.java @@ -0,0 +1,14 @@ +package com.willfp.ecoenchants.enchantments; + +import java.util.List; + +/** + * Mock class for legacy package of eco enchants + */ +public class EcoEnchants { + + public static List values(){ + return null; // We don't care here. + } + +} diff --git a/impl/LegacyEcoEnchant/src/main/java/com/willfp/ecoenchants/enchantments/meta/EnchantmentTarget.java b/impl/LegacyEcoEnchant/src/main/java/com/willfp/ecoenchants/enchantments/meta/EnchantmentTarget.java new file mode 100644 index 0000000..19a667b --- /dev/null +++ b/impl/LegacyEcoEnchant/src/main/java/com/willfp/ecoenchants/enchantments/meta/EnchantmentTarget.java @@ -0,0 +1,16 @@ +package com.willfp.ecoenchants.enchantments.meta; + +import org.bukkit.Material; + +import java.util.Set; + +/** + * Mock class for legacy package of eco enchants + */ +public class EnchantmentTarget { + + public Set getMaterials() { + return null; + } + +} diff --git a/impl/LegacyEcoEnchant/src/main/java/com/willfp/ecoenchants/enchantments/meta/EnchantmentType.java b/impl/LegacyEcoEnchant/src/main/java/com/willfp/ecoenchants/enchantments/meta/EnchantmentType.java new file mode 100644 index 0000000..183ec74 --- /dev/null +++ b/impl/LegacyEcoEnchant/src/main/java/com/willfp/ecoenchants/enchantments/meta/EnchantmentType.java @@ -0,0 +1,9 @@ +package com.willfp.ecoenchants.enchantments.meta; + +public class EnchantmentType { + + public boolean isSingular() { + return false; + } + +} diff --git a/libs/AxPlayerWarps-1.10.3.jar b/libs/AxPlayerWarps-1.10.3.jar new file mode 100644 index 0000000..a32b23c Binary files /dev/null and b/libs/AxPlayerWarps-1.10.3.jar differ diff --git a/libs/Disenchantment-6.1.5.jar b/libs/Disenchantment-6.1.5.jar new file mode 100644 index 0000000..a574f38 Binary files /dev/null and b/libs/Disenchantment-6.1.5.jar differ diff --git a/libs/EnchantsSquared.jar b/libs/EnchantsSquared.jar new file mode 100644 index 0000000..add5768 Binary files /dev/null and b/libs/EnchantsSquared.jar differ diff --git a/libs/ExcellentEnchants-4.1.0-striped.jar b/libs/ExcellentEnchants-4.1.0-striped.jar new file mode 100644 index 0000000..a3500aa Binary files /dev/null and b/libs/ExcellentEnchants-4.1.0-striped.jar differ diff --git a/libs/ExcellentEnchants-4.3.3-striped.jar b/libs/ExcellentEnchants-4.3.3-striped.jar new file mode 100644 index 0000000..ff0656b Binary files /dev/null and b/libs/ExcellentEnchants-4.3.3-striped.jar differ diff --git a/libs/HavenBags-1.31.0.1760.jar b/libs/HavenBags-1.31.0.1760.jar new file mode 100644 index 0000000..aed122d Binary files /dev/null and b/libs/HavenBags-1.31.0.1760.jar differ diff --git a/libs/SuperEnchants-4.6.2-all.jar b/libs/SuperEnchants-4.6.2-all.jar new file mode 100644 index 0000000..f4c832a Binary files /dev/null and b/libs/SuperEnchants-4.6.2-all.jar differ diff --git a/libs/nightcore-2.7.3.jar b/libs/nightcore-2.7.3.jar new file mode 100644 index 0000000..7ecad8e Binary files /dev/null and b/libs/nightcore-2.7.3.jar differ diff --git a/libs/toolstats-1.9.6-stripped.jar b/libs/toolstats-1.9.6-stripped.jar new file mode 100644 index 0000000..eb17393 Binary files /dev/null and b/libs/toolstats-1.9.6-stripped.jar differ diff --git a/nms/nms-common/.gitignore b/nms/nms-common/.gitignore new file mode 100644 index 0000000..4f41170 --- /dev/null +++ b/nms/nms-common/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId diff --git a/nms/nms-common/build.gradle.kts b/nms/nms-common/build.gradle.kts new file mode 100644 index 0000000..950d807 --- /dev/null +++ b/nms/nms-common/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + // Used for nms + paperweight.paperDevBundle("1.21.10-R0.1-SNAPSHOT") + + // Protocolib + compileOnly("net.dmulloy2:ProtocolLib:5.4.0") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "16" + targetCompatibility = "16" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_16) + } +} diff --git a/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/NoPacketManager.kt b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/NoPacketManager.kt new file mode 100644 index 0000000..c3367f5 --- /dev/null +++ b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/NoPacketManager.kt @@ -0,0 +1,14 @@ +package xyz.alexcrea.cuanvil.dependency.packet + +import org.bukkit.entity.Player + +class NoPacketManager: PacketManager { + + override val canSetInstantBuild: Boolean + get() = false + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + // ProtocoLib not installed and not in a supported version: We do nothing + } + +} diff --git a/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManager.kt b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManager.kt new file mode 100644 index 0000000..857bfab --- /dev/null +++ b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManager.kt @@ -0,0 +1,17 @@ +package xyz.alexcrea.cuanvil.dependency.packet + +import org.bukkit.entity.Player + +interface PacketManager { + + /** + * If the provided packet manager if able to set instant build. + */ + val canSetInstantBuild: Boolean + + /** + * Try to set instant build properties + */ + fun setInstantBuild(player: Player, instantBuild: Boolean) + +} diff --git a/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerBase.kt b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerBase.kt new file mode 100644 index 0000000..184aa0e --- /dev/null +++ b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerBase.kt @@ -0,0 +1,16 @@ +package xyz.alexcrea.cuanvil.dependency.packet + +import org.bukkit.entity.Player +import org.bukkit.event.Listener + +abstract class PacketManagerBase() : PacketManager, Listener { + + override val canSetInstantBuild: Boolean + get() = false + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + // Default implementation is empty. + } + + +} diff --git a/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/ProtocoLibWrapper.kt b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/ProtocoLibWrapper.kt new file mode 100644 index 0000000..8143b6b --- /dev/null +++ b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/ProtocoLibWrapper.kt @@ -0,0 +1,39 @@ +package xyz.alexcrea.cuanvil.dependency.packet + +import com.comphenix.protocol.PacketType +import com.comphenix.protocol.ProtocolLibrary +import com.comphenix.protocol.ProtocolManager +import com.comphenix.protocol.events.PacketContainer +import org.bukkit.entity.Player +import java.lang.reflect.InvocationTargetException + +class ProtocoLibWrapper: PacketManager { + + private val protocolManager: ProtocolManager = ProtocolLibrary.getProtocolManager(); + + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val packet = PacketContainer(PacketType.Play.Server.ABILITIES) + + // Set player's properties + packet.float + .write(0, player.flySpeed / 2) + .write(1, player.walkSpeed / 2) + + packet.booleans + .write(0, player.isInvulnerable) + .write(1, player.isFlying) + .write(2, player.allowFlight) + .write(3, instantBuild) + + // Send packet + try { + protocolManager.sendServerPacket(player, packet) + } catch (e: InvocationTargetException) { + e.printStackTrace() + } + } + +} diff --git a/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/scheduler/TaskScheduler.kt b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/scheduler/TaskScheduler.kt new file mode 100644 index 0000000..e6a70dd --- /dev/null +++ b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/scheduler/TaskScheduler.kt @@ -0,0 +1,18 @@ +package xyz.alexcrea.cuanvil.dependency.scheduler + +import org.bukkit.entity.Entity +import org.bukkit.plugin.Plugin + +interface TaskScheduler { + + fun scheduleGlobally(plugin: Plugin, task: Runnable, time: Long): Any? + fun scheduleGlobally(plugin: Plugin, task: Runnable): Any?{ + return scheduleGlobally(plugin, task, 0L) + } + + fun scheduleOnEntity(plugin: Plugin, entity: Entity, task: Runnable, time: Long): Any? + fun scheduleOnEntity(plugin: Plugin, entity: Entity, task: Runnable): Any?{ + return scheduleOnEntity(plugin, entity, task, 0L) + } + +} \ No newline at end of file diff --git a/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/util/PaperSpigotUtil.kt b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/util/PaperSpigotUtil.kt new file mode 100644 index 0000000..3f709c7 --- /dev/null +++ b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/util/PaperSpigotUtil.kt @@ -0,0 +1,102 @@ +package xyz.alexcrea.cuanvil.dependency.util + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.ItemMeta + +// Mostly made for paper, spigot and folia support +@Suppress("DEPRECATION") +object PlatformUtil { + + private fun hasClass(className: String): Boolean { + try { + Class.forName(className) + return true + } catch (_: ClassNotFoundException) { + return false + } + } + + private fun hasMethod(clazz: Class<*>, name: String, vararg parameterTypes: Class<*>): Boolean { + try { + clazz.getDeclaredMethod(name, *parameterTypes) + return true + } catch (_: NoSuchMethodException) { + return false + } + } + + val isPaper = hasClass("com.destroystokyo.paper.PaperConfig") || + hasClass("io.papermc.paper.configuration.Configuration") + + val isFolia = hasClass("io.papermc.paper.threadedregions.RegionizedServer") + + private val legacy_mm = LegacyComponentSerializer.legacySection() + + // Lore + fun ItemMeta.componentLore(): MutableList { + val lore: List? + if(isPaper){ + lore = this.lore() + } else { + val legacyLores = this.lore ?: return ArrayList() + + lore = ArrayList(legacyLores.size) + for (legacyLore in legacyLores) { + lore.add(legacy_mm.deserialize(legacyLore)) + } + } + + return lore ?: ArrayList() + } + + fun ItemMeta.setComponentLore(lore: List) { + if(isPaper){ + this.lore(lore) + } else { + val legacyLore = ArrayList(lore.size) + for (component in lore) { + legacyLore.add(if(component == null) null + else legacy_mm.serialize(component)) + } + + this.lore = legacyLore + } + } + + // Display name + private val useCustomName = hasMethod(ItemStack::class.java, "customName") + + fun ItemMeta.componentDisplayName(): Component? { + if(useCustomName){ + if(!this.hasCustomName()) return null + return this.customName() + }else if(isPaper){ + if(!this.hasDisplayName()) return null + return this.displayName() + } else { + if(!this.hasDisplayName()) return null + + val legacy = this.displayName + return legacy_mm.deserialize(legacy) + } + } + + fun ItemMeta.setComponentDisplayName(component: Component?) { + if(useCustomName){ + this.customName(component) + }else if(isPaper){ + this.displayName(component) + } else { + if(component == null){ + this.setDisplayName(null) + return + } + + val legacy = legacy_mm.serialize(component) + this.setDisplayName(legacy) + } + } + +} diff --git a/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dialog/AnvilRenameDialog.kt b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dialog/AnvilRenameDialog.kt new file mode 100644 index 0000000..df6e883 --- /dev/null +++ b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dialog/AnvilRenameDialog.kt @@ -0,0 +1,23 @@ +package xyz.alexcrea.cuanvil.dialog + +import org.bukkit.NamespacedKey +import org.bukkit.entity.HumanEntity +import org.bukkit.event.inventory.PrepareAnvilEvent + +interface AnvilRenameDialog { + + companion object { + val PCD_KEEP_RENAME_TEXT_KEY = NamespacedKey.fromString("customanvil:last_rename_text")!! + } + + fun canSendDialog(): Boolean + + fun tryShowDialog(player: HumanEntity, event: PrepareAnvilEvent) + + fun closeInventory(player: HumanEntity) + + fun currentText(player: HumanEntity): String? + + fun isOpenFor(player: HumanEntity): Boolean + +} \ No newline at end of file diff --git a/nms/nms-paper/.gitignore b/nms/nms-paper/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/nms-paper/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/nms-paper/build.gradle.kts b/nms/nms-paper/build.gradle.kts new file mode 100644 index 0000000..d0e12a7 --- /dev/null +++ b/nms/nms-paper/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.21.7-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "18" + targetCompatibility = "18" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_18) + } +} diff --git a/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/datapack/DataPackTester.kt b/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/datapack/DataPackTester.kt new file mode 100644 index 0000000..9003fdf --- /dev/null +++ b/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/datapack/DataPackTester.kt @@ -0,0 +1,38 @@ +package xyz.alexcrea.cuanvil.dependency.datapack + +import io.papermc.paper.datapack.Datapack +import org.bukkit.Bukkit +import org.bukkit.packs.DataPack +import java.util.* + +object DataPackTester { + val legacyNames: List + get() = Bukkit.getDataPackManager().dataPacks + .stream().filter { obj -> obj.isEnabled } + .map { pack -> pack.key.key } + .toList() + + val enabledPacks: List + get() { + try { + // will throw error if do not exist + Bukkit::class.java.getDeclaredMethod("getDatapackManager") + + return Bukkit.getDatapackManager().enabledPacks + .stream().map { obj: Datapack -> obj.name } + .toList() + } catch (e: NoSuchMethodException) { + try { + DataPack::class.java.getDeclaredMethod("getKey") + } catch (e: NoSuchMethodException) { + System.err.println("Could not find compatible datapack manager") + System.err.println("If you are using a datapack that should be compatible with CustomAnvil. It will not get detected...") + return emptyList() + } + return legacyNames + } catch (e: Exception){ + // Assume cause UnimplementedOperationException on mock server + return Collections.emptyList() + } + } +} diff --git a/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/PaperPacketManager.kt b/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/PaperPacketManager.kt new file mode 100644 index 0000000..ee0c101 --- /dev/null +++ b/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/PaperPacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class PaperPacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.setFlyingSpeed(playerAbilities.getFlyingSpeed()) + sendedAbilities.setWalkingSpeed(playerAbilities.getWalkingSpeed()) + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/scheduler/FoliaScheduler.kt b/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/scheduler/FoliaScheduler.kt new file mode 100644 index 0000000..a9bd79f --- /dev/null +++ b/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/scheduler/FoliaScheduler.kt @@ -0,0 +1,40 @@ +package xyz.alexcrea.cuanvil.dependency.scheduler + +import io.papermc.paper.threadedregions.scheduler.ScheduledTask +import org.bukkit.Bukkit +import org.bukkit.entity.Entity +import org.bukkit.plugin.Plugin +import java.util.function.Consumer + +class FoliaScheduler : TaskScheduler { + override fun scheduleGlobally(plugin: Plugin, task: Runnable, time: Long): Any? { + if(time < 1){ + return Bukkit.getGlobalRegionScheduler().run( + plugin + ) { scheduledTask: ScheduledTask? -> task.run() } + } + return Bukkit.getGlobalRegionScheduler().runDelayed( + plugin, + { scheduledTask: ScheduledTask? -> task.run() }, + time + ) + } + + + override fun scheduleOnEntity(plugin: Plugin, entity: Entity, task: Runnable, time: Long): Any? { + if(time < 1){ + return entity.scheduler.run( + plugin, + { scheduledTask: ScheduledTask? -> task.run() }, + {} + ) + } + return entity.scheduler.runDelayed( + plugin, + { scheduledTask: ScheduledTask? -> task.run() }, + {}, + time + ) + } + +} diff --git a/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/dialog/AnvilRenameDialogImpl.kt b/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/dialog/AnvilRenameDialogImpl.kt new file mode 100644 index 0000000..e210daa --- /dev/null +++ b/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/dialog/AnvilRenameDialogImpl.kt @@ -0,0 +1,236 @@ +package xyz.alexcrea.cuanvil.dialog + +import io.papermc.paper.dialog.Dialog +import io.papermc.paper.registry.data.dialog.ActionButton +import io.papermc.paper.registry.data.dialog.DialogBase +import io.papermc.paper.registry.data.dialog.action.DialogAction +import io.papermc.paper.registry.data.dialog.body.DialogBody +import io.papermc.paper.registry.data.dialog.input.DialogInput +import io.papermc.paper.registry.data.dialog.type.DialogType +import io.papermc.paper.threadedregions.scheduler.ScheduledTask +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.event.ClickCallback +import net.kyori.adventure.text.format.TextColor +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import net.minecraft.world.inventory.AnvilMenu +import org.bukkit.craftbukkit.event.CraftEventFactory +import org.bukkit.craftbukkit.inventory.CraftInventoryView +import org.bukkit.craftbukkit.inventory.view.CraftAnvilView +import org.bukkit.entity.HumanEntity +import org.bukkit.event.inventory.PrepareAnvilEvent +import org.bukkit.inventory.InventoryView +import org.bukkit.inventory.ItemStack +import org.bukkit.persistence.PersistentDataType +import org.bukkit.plugin.Plugin +import java.util.* +import java.util.function.BiFunction +import java.util.function.Consumer +import java.util.function.Supplier + +@Suppress("UnstableApiUsage") +class AnvilRenameDialogImpl( + val fromFormated: BiFunction, + val keepUserPreviousDialog: Supplier, + val maxLength: Supplier, + val plugin: Plugin, +) : AnvilRenameDialog { + + companion object { + private const val RENAME_TEXT_KEY = "rename" + + private const val MAX_WIDTH = 512 + + private val PLAIN_TEXT_SERIALIZER = PlainTextComponentSerializer.plainText() + + // Need to be able to translate it later ! + private val USER_FACING_RENAME_TITLE = Component.text("Rename Your Item") + private val USER_FACING_WARNING = Component.text( + "Note that the repair text will appear blank after Confirm\n" + + "But the name will be correctly applied" + ) + private val USER_FACING_CONFIRM = Component.text("Confirm").color(TextColor.fromHexString("#40FF40")) + private val USER_FACING_CANCEL = Component.text("Cancel").color(TextColor.fromHexString("#FF4040")) + + fun itemDefaultName(item: ItemStack?): String? { + return PLAIN_TEXT_SERIALIZER.serializeOrNull(item?.effectiveName()) + } + } + + private val lastNames = HashMap() + private val lastRenames = HashMap() + + + private val lastLeftItem = HashMap() + private val runTaskMap = HashMap() + + // For monetary cost + val hasUiOpen = HashSet() + + private val containerField = CraftInventoryView::class.java.getDeclaredField("container") + + init { + containerField.setAccessible(true) + } + + override fun canSendDialog(): Boolean { + return true + } + + fun makeDialog(playerID: UUID, initial: String?, callback: Consumer): Dialog { + val maxLength = this.maxLength.get() + val initialFinal = initial?.take(maxLength) + + val baseBuilder = DialogBase.builder(USER_FACING_RENAME_TITLE) + .canCloseWithEscape(true) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .inputs( + listOf( + DialogInput.text(RENAME_TEXT_KEY, Component.text("Rename text")) + .maxLength(maxLength) + .initial(initialFinal ?: "") + .labelVisible(false) + .width(MAX_WIDTH) + .build(), + ), + ) + baseBuilder.body( + listOf( + DialogBody.plainMessage(USER_FACING_WARNING, MAX_WIDTH) + ) + ) + + return Dialog.create { builder -> + builder.empty() + .base(baseBuilder.build()) + .type( + DialogType.confirmation( + ActionButton.builder(USER_FACING_CONFIRM) + .action(DialogAction.customClick({ response, _ -> + hasUiOpen.remove(playerID) + val text = response.getText(RENAME_TEXT_KEY)!! + callback.accept(text) + }, ClickCallback.Options.builder().build())) + .build(), + ActionButton.builder(USER_FACING_CANCEL) + .action(DialogAction.customClick({ response, _ -> + hasUiOpen.remove(playerID) + }, ClickCallback.Options.builder().build())) + .build(), + ) + ) + } + } + + private fun setResult(player: HumanEntity, view: InventoryView, result: String?) { + val defaultName = itemDefaultName(view.getItem(0)) + if (defaultName == result) { + setName(player, view, "", null) + if (defaultName != null) lastNames[player.uniqueId] = defaultName + } else setName(player, view, result, result) + } + + private fun setName(player: HumanEntity, view: InventoryView, name: String?, rename: String?) { + val menu = (containerField.get(view) as AnvilMenu) + val isSameName = menu.itemName == name + menu.itemName = rename + + if (name == null) + lastNames.remove(player.uniqueId) + else + lastNames[player.uniqueId] = name + + if (rename == null) + lastRenames.remove(player.uniqueId) + else + lastRenames[player.uniqueId] = rename + + if (!isSameName) + CraftEventFactory.callPrepareResultEvent(menu, 2); + } + + private fun nameFromItem(player: HumanEntity, item: ItemStack?): String? { + // Already has text + if (item?.hasItemMeta() != true || !item.itemMeta.hasCustomName()) + return PLAIN_TEXT_SERIALIZER.serializeOrNull(item?.effectiveName()) + + if (keepUserPreviousDialog.get() && item.hasItemMeta()) { + val lastName = item.itemMeta.persistentDataContainer.get( + AnvilRenameDialog.PCD_KEEP_RENAME_TEXT_KEY, + PersistentDataType.STRING + ) + + if (lastName != null) return lastName + } + + return fromFormated.apply(player, item.effectiveName()) + } + + private fun tryShowDialogScheduled(player: HumanEntity, event: PrepareAnvilEvent) { + val view = event.view + if (view !is CraftAnvilView) return + + val renameText = view.renameText + val leftItem = view.getItem(0) + val leftItemStr = leftItem?.toString() + + val lastName = lastNames.getOrDefault(player.uniqueId, null) + val lastRename = lastRenames.getOrDefault(player.uniqueId, null) + + if (lastLeftItem.getOrDefault(player.uniqueId, null) != leftItemStr) { + if (leftItemStr == null) + lastLeftItem.remove(player.uniqueId) + else lastLeftItem[player.uniqueId] = leftItemStr + + setName(player, view, renameText, nameFromItem(player, leftItem)) + return + } + + if (lastName == renameText || lastRename == renameText) + return + + if (renameText?.isBlank() == true || renameText == itemDefaultName(leftItem)) { + setName(player, view, lastName, lastRename) + return + } + + val dialog = makeDialog(player.uniqueId, lastRename) + { result -> setResult(player, view, result) } + player.showDialog(dialog) + + hasUiOpen.add(player.uniqueId) + } + + // We need to wait for a short time as changing item will change the name BEFORE the item change + // no guaranty both of them came in the same tick too so let's wait 2 tick.... + override fun tryShowDialog(player: HumanEntity, event: PrepareAnvilEvent) { + runTaskMap.remove(player.uniqueId)?.cancel() + + val task = player.scheduler.runDelayed( + plugin, + { _ -> + run { tryShowDialogScheduled(player, event) } + }, + {}, + 2 + ) + if (task == null) return + + runTaskMap[player.uniqueId] = task + } + + override fun closeInventory(player: HumanEntity) { + lastNames.remove(player.uniqueId) + lastRenames.remove(player.uniqueId) + lastLeftItem.remove(player.uniqueId) + runTaskMap.remove(player.uniqueId)?.cancel() + } + + override fun currentText(player: HumanEntity): String? { + return lastNames[player.uniqueId] + } + + override fun isOpenFor(player: HumanEntity): Boolean { + return hasUiOpen.contains(player.uniqueId) + } + +} \ No newline at end of file diff --git a/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilTitleUtil.kt b/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilTitleUtil.kt new file mode 100644 index 0000000..e40e7ed --- /dev/null +++ b/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilTitleUtil.kt @@ -0,0 +1,45 @@ +package xyz.alexcrea.cuanvil.util + +import io.papermc.paper.threadedregions.scheduler.ScheduledTask +import org.bukkit.entity.HumanEntity +import org.bukkit.inventory.InventoryView +import org.bukkit.plugin.Plugin +import xyz.alexcrea.cuanvil.dialog.AnvilRenameDialog +import java.util.HashMap +import java.util.UUID + +object AnvilTitleUtil { + + private val runTaskMap = HashMap() + + private fun actualRename(view: InventoryView, name: String, player: HumanEntity, anvilDialog: AnvilRenameDialog) { + runTaskMap.remove(player.uniqueId) + if (view.title == name) return + + // We assume rename impl is used + if (anvilDialog.isOpenFor(player)) return + + view.title = name + } + + // We don't want to rename instantly it is causing issue with rename text + // especially as it can "override" current ui when it is rename ui time but rename ui also need some delay + fun rename(view: InventoryView, name: String, player: HumanEntity, anvilDialog: AnvilRenameDialog, plugin: Plugin) { + runTaskMap.remove(player.uniqueId)?.cancel() + + val task = player.scheduler.runDelayed( + plugin, + { _ -> + run { actualRename(view, name, player, anvilDialog) } + }, + { + runTaskMap.remove(player.uniqueId) + }, + 2 + ) + + if (task == null) return + runTaskMap[player.uniqueId] = task + } + +} \ No newline at end of file diff --git a/nms/v1_17R1/.gitignore b/nms/v1_17R1/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_17R1/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_17R1/build.gradle.kts b/nms/v1_17R1/build.gradle.kts new file mode 100644 index 0000000..c354b1c --- /dev/null +++ b/nms/v1_17R1/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.17.1-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "16" + targetCompatibility = "16" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_16) + } +} diff --git a/nms/v1_17R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_17R1_PacketManager.kt b/nms/v1_17R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_17R1_PacketManager.kt new file mode 100644 index 0000000..c820eab --- /dev/null +++ b/nms/v1_17R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_17R1_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.v1_17_R1.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_17R1_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/v1_18R1/.gitignore b/nms/v1_18R1/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_18R1/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_18R1/build.gradle.kts b/nms/v1_18R1/build.gradle.kts new file mode 100644 index 0000000..44873d5 --- /dev/null +++ b/nms/v1_18R1/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.18.1-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "17" + targetCompatibility = "17" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_17) + } +} diff --git a/nms/v1_18R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_18R1_PacketManager.kt b/nms/v1_18R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_18R1_PacketManager.kt new file mode 100644 index 0000000..71df5c7 --- /dev/null +++ b/nms/v1_18R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_18R1_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.v1_18_R1.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_18R1_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/v1_18R2/.gitignore b/nms/v1_18R2/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_18R2/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_18R2/build.gradle.kts b/nms/v1_18R2/build.gradle.kts new file mode 100644 index 0000000..cf349ec --- /dev/null +++ b/nms/v1_18R2/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.18.2-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "17" + targetCompatibility = "17" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_17) + } +} diff --git a/nms/v1_18R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_18R2_PacketManager.kt b/nms/v1_18R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_18R2_PacketManager.kt new file mode 100644 index 0000000..ee442f5 --- /dev/null +++ b/nms/v1_18R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_18R2_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.v1_18_R2.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_18R2_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/v1_19R1/.gitignore b/nms/v1_19R1/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_19R1/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_19R1/build.gradle.kts b/nms/v1_19R1/build.gradle.kts new file mode 100644 index 0000000..9791b9b --- /dev/null +++ b/nms/v1_19R1/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.19.2-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "17" + targetCompatibility = "17" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_17) + } +} diff --git a/nms/v1_19R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_19R1_PacketManager.kt b/nms/v1_19R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_19R1_PacketManager.kt new file mode 100644 index 0000000..7a057fc --- /dev/null +++ b/nms/v1_19R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_19R1_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.v1_19_R1.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_19R1_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/v1_19R2/.gitignore b/nms/v1_19R2/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_19R2/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_19R2/build.gradle.kts b/nms/v1_19R2/build.gradle.kts new file mode 100644 index 0000000..88f1a8a --- /dev/null +++ b/nms/v1_19R2/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.19.3-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "17" + targetCompatibility = "17" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_17) + } +} diff --git a/nms/v1_19R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_19R2_PacketManager.kt b/nms/v1_19R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_19R2_PacketManager.kt new file mode 100644 index 0000000..0d04cd1 --- /dev/null +++ b/nms/v1_19R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_19R2_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.v1_19_R2.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_19R2_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/v1_19R3/.gitignore b/nms/v1_19R3/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_19R3/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_19R3/build.gradle.kts b/nms/v1_19R3/build.gradle.kts new file mode 100644 index 0000000..3d609af --- /dev/null +++ b/nms/v1_19R3/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.19.4-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "17" + targetCompatibility = "17" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_17) + } +} diff --git a/nms/v1_19R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_19R3_PacketManager.kt b/nms/v1_19R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_19R3_PacketManager.kt new file mode 100644 index 0000000..7c72791 --- /dev/null +++ b/nms/v1_19R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_19R3_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.v1_19_R3.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_19R3_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/v1_20R1/.gitignore b/nms/v1_20R1/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_20R1/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_20R1/build.gradle.kts b/nms/v1_20R1/build.gradle.kts new file mode 100644 index 0000000..67b46b5 --- /dev/null +++ b/nms/v1_20R1/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.20.1-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "18" + targetCompatibility = "18" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_18) + } +} diff --git a/nms/v1_20R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_20R1_PacketManager.kt b/nms/v1_20R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_20R1_PacketManager.kt new file mode 100644 index 0000000..1fbac83 --- /dev/null +++ b/nms/v1_20R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_20R1_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_20R1_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/v1_20R2/.gitignore b/nms/v1_20R2/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_20R2/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_20R2/build.gradle.kts b/nms/v1_20R2/build.gradle.kts new file mode 100644 index 0000000..e8417cd --- /dev/null +++ b/nms/v1_20R2/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.20.2-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "18" + targetCompatibility = "18" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_18) + } +} diff --git a/nms/v1_20R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_20R2_PacketManager.kt b/nms/v1_20R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_20R2_PacketManager.kt new file mode 100644 index 0000000..a2db371 --- /dev/null +++ b/nms/v1_20R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_20R2_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.v1_20_R2.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_20R2_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/v1_20R3/.gitignore b/nms/v1_20R3/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_20R3/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_20R3/build.gradle.kts b/nms/v1_20R3/build.gradle.kts new file mode 100644 index 0000000..c49ff48 --- /dev/null +++ b/nms/v1_20R3/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.20.4-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "18" + targetCompatibility = "18" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_18) + } +} diff --git a/nms/v1_20R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_20R3_PacketManager.kt b/nms/v1_20R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_20R3_PacketManager.kt new file mode 100644 index 0000000..51c2ecb --- /dev/null +++ b/nms/v1_20R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_20R3_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.v1_20_R3.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_20R3_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/v1_20R4/.gitignore b/nms/v1_20R4/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_20R4/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_20R4/build.gradle.kts b/nms/v1_20R4/build.gradle.kts new file mode 100644 index 0000000..3b98361 --- /dev/null +++ b/nms/v1_20R4/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.20.6-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "18" + targetCompatibility = "18" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0) + jvmTarget.set(JvmTarget.JVM_18) + } +} diff --git a/nms/v1_20R4/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_20R4_PacketManager.kt b/nms/v1_20R4/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_20R4_PacketManager.kt new file mode 100644 index 0000000..ac1e504 --- /dev/null +++ b/nms/v1_20R4/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_20R4_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_20R4_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/v1_20R4/src/main/kotlin/xyz/alexcrea/cuanvil/util/MaxDamageCheckerUtil.kt b/nms/v1_20R4/src/main/kotlin/xyz/alexcrea/cuanvil/util/MaxDamageCheckerUtil.kt new file mode 100644 index 0000000..b3ce0ff --- /dev/null +++ b/nms/v1_20R4/src/main/kotlin/xyz/alexcrea/cuanvil/util/MaxDamageCheckerUtil.kt @@ -0,0 +1,18 @@ +package xyz.alexcrea.cuanvil.util + +import org.bukkit.inventory.meta.Damageable + +// I LOVE support of old versions and needing to do modules like that +// That truly is my favorite activity +// TODO clean this one of legacy removal branch +object MaxDamageCheckerUtil { + + /** + * @return max damage or int max if not set + */ + fun getMaxDamage(meta: Damageable): Int { + if(!meta.hasMaxDamage()) return Integer.MAX_VALUE + return meta.maxDamage + } + +} \ No newline at end of file diff --git a/nms/v1_21R1/.gitignore b/nms/v1_21R1/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_21R1/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_21R1/build.gradle.kts b/nms/v1_21R1/build.gradle.kts new file mode 100644 index 0000000..bf2152c --- /dev/null +++ b/nms/v1_21R1/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.21.1-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "21" + targetCompatibility = "21" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_21) + } +} diff --git a/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R1_PacketManager.kt b/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R1_PacketManager.kt new file mode 100644 index 0000000..4ea9950 --- /dev/null +++ b/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R1_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_21R1_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/v1_21R2/.gitignore b/nms/v1_21R2/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_21R2/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_21R2/build.gradle.kts b/nms/v1_21R2/build.gradle.kts new file mode 100644 index 0000000..e65f6fd --- /dev/null +++ b/nms/v1_21R2/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.21.3-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "21" + targetCompatibility = "21" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_21) + } +} diff --git a/nms/v1_21R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R2_PacketManager.kt b/nms/v1_21R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R2_PacketManager.kt new file mode 100644 index 0000000..9d88c20 --- /dev/null +++ b/nms/v1_21R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R2_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_21R2_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/v1_21R3/.gitignore b/nms/v1_21R3/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_21R3/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_21R3/build.gradle.kts b/nms/v1_21R3/build.gradle.kts new file mode 100644 index 0000000..d06816c --- /dev/null +++ b/nms/v1_21R3/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "21" + targetCompatibility = "21" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_21) + } +} diff --git a/nms/v1_21R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R3_PacketManager.kt b/nms/v1_21R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R3_PacketManager.kt new file mode 100644 index 0000000..0f581d9 --- /dev/null +++ b/nms/v1_21R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R3_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_21R3_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/v1_21R4/.gitignore b/nms/v1_21R4/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_21R4/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_21R4/build.gradle.kts b/nms/v1_21R4/build.gradle.kts new file mode 100644 index 0000000..8bd22b9 --- /dev/null +++ b/nms/v1_21R4/build.gradle.kts @@ -0,0 +1,34 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.21.5-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "21" + targetCompatibility = "21" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_21) + } +} diff --git a/nms/v1_21R4/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R4_PacketManager.kt b/nms/v1_21R4/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R4_PacketManager.kt new file mode 100644 index 0000000..b123625 --- /dev/null +++ b/nms/v1_21R4/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R4_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_21R4_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/v1_21R5/.gitignore b/nms/v1_21R5/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_21R5/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_21R5/build.gradle.kts b/nms/v1_21R5/build.gradle.kts new file mode 100644 index 0000000..e7f7c47 --- /dev/null +++ b/nms/v1_21R5/build.gradle.kts @@ -0,0 +1,34 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.21.6-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "21" + targetCompatibility = "21" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_21) + } +} diff --git a/nms/v1_21R5/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R5_PacketManager.kt b/nms/v1_21R5/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R5_PacketManager.kt new file mode 100644 index 0000000..561a6af --- /dev/null +++ b/nms/v1_21R5/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R5_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_21R5_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} diff --git a/nms/v1_21R6/.gitignore b/nms/v1_21R6/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_21R6/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_21R6/build.gradle.kts b/nms/v1_21R6/build.gradle.kts new file mode 100644 index 0000000..3189b5c --- /dev/null +++ b/nms/v1_21R6/build.gradle.kts @@ -0,0 +1,34 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.21.10-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "21" + targetCompatibility = "21" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_21) + } +} diff --git a/nms/v1_21R6/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/PaperPacketManager.kt b/nms/v1_21R6/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/PaperPacketManager.kt new file mode 100644 index 0000000..ee00666 --- /dev/null +++ b/nms/v1_21R6/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/PaperPacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_21R6_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} \ No newline at end of file diff --git a/nms/v1_21R7/.gitignore b/nms/v1_21R7/.gitignore new file mode 100644 index 0000000..47374f1 --- /dev/null +++ b/nms/v1_21R7/.gitignore @@ -0,0 +1 @@ +.lastDeploymentsId \ No newline at end of file diff --git a/nms/v1_21R7/build.gradle.kts b/nms/v1_21R7/build.gradle.kts new file mode 100644 index 0000000..91c3111 --- /dev/null +++ b/nms/v1_21R7/build.gradle.kts @@ -0,0 +1,34 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +group = rootProject.group +version = rootProject.version + +plugins { + id("io.papermc.paperweight.userdev") +} + +dependencies { + implementation(project(":nms:nms-common")) + + // Used for nms + paperweight.paperDevBundle("1.21.11-R0.1-SNAPSHOT") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") +} + +// Set target version +tasks.withType().configureEach { + sourceCompatibility = "21" + targetCompatibility = "21" + + options.encoding = "UTF-8" +} + +kotlin { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + jvmTarget.set(JvmTarget.JVM_21) + } +} diff --git a/nms/v1_21R7/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R7_PacketManager.kt b/nms/v1_21R7/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R7_PacketManager.kt new file mode 100644 index 0000000..59ae9ce --- /dev/null +++ b/nms/v1_21R7/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R7_PacketManager.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.dependency.packet.versions + +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket +import net.minecraft.world.entity.player.Abilities +import org.bukkit.craftbukkit.entity.CraftPlayer +import org.bukkit.entity.Player +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase + +class V1_21R7_PacketManager : PacketManagerBase(), PacketManager { + override val canSetInstantBuild: Boolean + get() = true + + override fun setInstantBuild(player: Player, instantBuild: Boolean) { + val nmsPlayer = (player as CraftPlayer).handle + val playerAbilities = nmsPlayer.abilities + val sendedAbilities: Abilities + if (playerAbilities.instabuild == instantBuild) { + sendedAbilities = playerAbilities + } else { + sendedAbilities = Abilities() + sendedAbilities.invulnerable = playerAbilities.invulnerable + sendedAbilities.flying = playerAbilities.flying + sendedAbilities.mayfly = playerAbilities.mayfly + sendedAbilities.instabuild = instantBuild + sendedAbilities.mayBuild = playerAbilities.mayBuild + sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed + sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + } + val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) + nmsPlayer.connection.send(packet) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 6ffcb1a..9de7d8c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,22 @@ -rootProject.name = "UnsafeEnchants" +rootProject.name = "CustomAnvil" +// NMS subproject +include("nms:nms-common") +findProject(":nms:nms-common")?.name = "nms-common" +include("nms:nms-paper") +findProject(":nms:nms-paper")?.name = "nms-paper" + + +val reobfNMS = providers.gradleProperty("subprojects.reobfnms") + .get().split(",") + +for (nmsPart in reobfNMS) { + include("nms:$nmsPart") + findProject(":nms:$nmsPart")?.name = nmsPart +} + +// compatibility subprojects +include(":impl:LegacyEcoEnchant") +findProject(":impl:LegacyEcoEnchant")?.name = "LegacyEcoEnchant" +include("impl:ExcellentEnchant5_4") +findProject(":impl:ExcellentEnchant5_4")?.name = "ExcellentEnchant5_4" \ No newline at end of file diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/AnvilRecipeBuilder.java b/src/main/java/xyz/alexcrea/cuanvil/api/AnvilRecipeBuilder.java new file mode 100644 index 0000000..4292fa0 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/api/AnvilRecipeBuilder.java @@ -0,0 +1,283 @@ +package xyz.alexcrea.cuanvil.api; + +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe; + +/** + * A Builder for custom craft using anvil. + */ +@SuppressWarnings("unused") +public class AnvilRecipeBuilder { + + private @NotNull String name; + private boolean exactCount; + + private int levelCostPerCraft; + private int linearXpCostPerCraft; + + private boolean removeExactLinearXp; + + private @Nullable ItemStack leftItem; + private @Nullable ItemStack rightItem; + private @Nullable ItemStack resultItem; + + /** + * Instantiates a new Anvil recipe builder. + * exact count default to true. + * xp level and linear cost per craft default to 0. + * + * @param name The recipe name + */ + public AnvilRecipeBuilder(@NotNull String name) { + this.name = name; + + this.exactCount = true; + this.levelCostPerCraft = 0; + this.linearXpCostPerCraft = 0; + this.removeExactLinearXp = false; + + this.leftItem = null; + this.rightItem = null; + this.resultItem = null; + } + + /** + * Gets the recipe name. + * + * @return This recipe builder instance. + */ + @NotNull + public String getName() { + return name; + } + + /** + * Sets the recipe name. + * + * @param name The recipe name + * @return This recipe builder instance. + */ + public AnvilRecipeBuilder setName(String name) { + this.name = name; + return this; + } + + /** + * Get if the recipe is exact count. (default 0) + *

+ * Exact count mean the recipe can only be crafted 1 by 1. + * If set to false, then it will craft as much as possible in 1 go and will keep unused material onto the anvil inventory. + * + * @return If the recipe is exact count. + */ + public boolean isExactCount() { + return exactCount; + } + + /** + * Sets if the recipe is exact count. + *

+ * Exact count mean the recipe can only be crafted 1 by 1. + * If set to false, then it will craft as much as possible in 1 go and will keep unused material onto the anvil inventory. + * + * @param exactCount If the recipe is exact count + * @return This recipe builder instance. + */ + public AnvilRecipeBuilder setExactCount(boolean exactCount) { + this.exactCount = exactCount; + return this; + } + + /** + * Get the xp level cost per craft. (default 0) + * + * @return The xp level cost per craft + * @deprecated use {@link #getLevelCostPerCraft() getLevelCostPerCraft} instead + */ + @Deprecated(since = "1.13.0") + public int getXpCostPerCraft() { + return getLevelCostPerCraft(); + } + + /** + * Sets the xp level cost per craft. + * + * @param xpCostPerCraft The xp level cost per craft + * @return This recipe builder instance. + * @deprecated use {@link #setLevelCostPerCraft(int) setLevelCostPerCraft} instead + */ + @Deprecated(since = "1.13.0") + public AnvilRecipeBuilder setXpCostPerCraft(int xpCostPerCraft) { + return setLevelCostPerCraft(xpCostPerCraft); + } + + /** + * Get the xp level cost per craft. (default 0) + * + * @return The xp level cost per craft + */ + public int getLevelCostPerCraft() { + return levelCostPerCraft; + } + + /** + * Sets the xp level cost per craft. + * + * @param levelCostPerCraft The xp level cost per craft + * @return This recipe builder instance. + */ + public AnvilRecipeBuilder setLevelCostPerCraft(int levelCostPerCraft) { + this.levelCostPerCraft = levelCostPerCraft; + return this; + } + + /** + * Get the linear xp cost (not xp level cost) per craft. + * + * @return The xp level cost per craft + */ + public int getLinearXpCostPerCraft() { + return linearXpCostPerCraft; + } + + /** + * Sets the linear xp cost (not xp level cost) per craft. + * + * @param linearXpCostPerCraft The linear xp cost per craft + * @return This recipe builder instance. + */ + public AnvilRecipeBuilder setLinearXpCostPerCraft(int linearXpCostPerCraft) { + this.linearXpCostPerCraft = linearXpCostPerCraft; + return this; + } + + /** + * Get if the linear xp should get removed by an exact amount. + *

+ * If false (default) level cost will be the level that would be reached by a player with this amount of xp. + * If true will require the level that has at least the specified level of xp then on click remove only the necessary xp + *

+ * linear xp cost are applied after level cost + * @return if we should remove the exact amount of linear xp + */ + public boolean isRemoveExactLinearXp() { + return removeExactLinearXp; + } + + /** + * Set if the linear xp should get removed by an exact amount. + *

+ * If false (default) level cost will be the level that would be reached by a player with this amount of xp. + * If true will require the level that has at least the specified level of xp then on click remove only the necessary xp + *

+ * linear xp cost are applied after level cost + * @param removeExactLinearXp if we should remove the exact amount of linear xp + * @return This recipe builder instance. + */ + public AnvilRecipeBuilder setRemoveExactLinearXp(boolean removeExactLinearXp) { + this.removeExactLinearXp = removeExactLinearXp; + return this; + } + + /** + * Get the left item of the recipe. + * If null (default) then the recipe will not be able to be registered. + * + * @return The left item + */ + @Nullable + public ItemStack getLeftItem() { + return leftItem; + } + + /** + * Set the left item. + * If null (default) then the recipe will not be able to be registered. + * + * @param leftItem the left item + * @return This recipe builder instance. + */ + public AnvilRecipeBuilder setLeftItem(ItemStack leftItem) { + this.leftItem = leftItem; + return this; + } + + /** + * Get the recipe right item. + * null on default new instance. + * + * @return The right item + */ + @Nullable + public ItemStack getRightItem() { + return rightItem; + } + + /** + * Set the recipe right item. + * null on default new instance. + * + * @param rightItem the right item + * @return This recipe builder instance. + */ + public AnvilRecipeBuilder setRightItem(ItemStack rightItem) { + this.rightItem = rightItem; + return this; + } + + /** + * Get the recipe result item. + * If null (default) then the recipe will not be able to be registered. + * + * @return The result item + */ + @Nullable + public ItemStack getResultItem() { + return resultItem; + } + + /** + * Set the recipe result item. + * If null (default) then the recipe will not be able to be registered. + * + * @param resultItem The result item + * @return This recipe builder instance. + */ + public AnvilRecipeBuilder setResultItem(ItemStack resultItem) { + this.resultItem = resultItem; + return this; + } + + /** + * Build the anvil custom recipe. + * Should probably use {@link #registerIfAbsent() registerIfAbsent} or {@link ConflictAPI#addConflict(ConflictBuilder) addConflict}. + * + * @return A new anvil custom recipe base on this builder. + */ + @Nullable // null if missing argument + public AnvilCustomRecipe build() { + if (leftItem == null || resultItem == null) return null; + + return new AnvilCustomRecipe( + this.name, + this.exactCount, + this.levelCostPerCraft, + this.linearXpCostPerCraft, + this.removeExactLinearXp, + this.leftItem, this.rightItem, this.resultItem + ); + } + + /** + * Register this recipe if absent. + * Equivalent to {@link ConflictAPI#addConflict(ConflictBuilder)} + * + * @return True if successful. + */ + public boolean registerIfAbsent() { + return CustomAnvilRecipeApi.addRecipe(this); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/ConflictAPI.java b/src/main/java/xyz/alexcrea/cuanvil/api/ConflictAPI.java new file mode 100644 index 0000000..fe2715e --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/api/ConflictAPI.java @@ -0,0 +1,196 @@ +package xyz.alexcrea.cuanvil.api; + +import io.delilaheve.CustomAnvil; +import org.bukkit.NamespacedKey; +import org.bukkit.configuration.file.FileConfiguration; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.dependency.DependencyManager; +import xyz.alexcrea.cuanvil.group.EnchantConflictGroup; +import xyz.alexcrea.cuanvil.gui.config.global.EnchantConflictGui; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Custom Anvil api for conflict registry. + */ +@SuppressWarnings("unused") +public class ConflictAPI { + + private ConflictAPI() { + } + + private static Object saveChangeTask = null; + private static Object reloadChangeTask = null; + + /** + * Write and add a conflict. + * Will not write the conflict if it already exists. + * Will not be successful if the conflict is empty. + * + * @param builder The conflict builder to be based on + * @return True if successful. + */ + public static boolean addConflict(@NotNull ConflictBuilder builder) { + return addConflict(builder, false); + } + + /** + * Write and add a conflict. + * Will not write the conflict if it already exists. + * Will not be successful if the conflict is empty. + * + * @param builder The conflict builder to be based on + * @param overrideDeleted If we should write even if the conflict was previously deleted. + * @return True if successful. + */ + public static boolean addConflict(@NotNull ConflictBuilder builder, boolean overrideDeleted) { + FileConfiguration config = ConfigHolder.CONFLICT_HOLDER.getConfig(); + + // Test if conflict can be added + if (!overrideDeleted && ConfigHolder.CONFLICT_HOLDER.isDeleted(builder.getName())) return false; + if (config.contains(builder.getName())) return false; + + if (!writeConflict(builder, false)) return false; + + EnchantConflictGroup conflict = builder.build(); + // Register conflict + ConfigHolder.CONFLICT_HOLDER.getConflictManager().addConflict(conflict); + + // Add conflict to gui + EnchantConflictGui conflictGui = EnchantConflictGui.getCurrentInstance(); + if (conflictGui != null) conflictGui.updateValueForGeneric(conflict, true); + + return true; + } + + /** + * Write a conflict to the config file and plan an update of conflicts. + *

+ * You may want to use {@link #addConflict(ConflictBuilder)} instead as it is more performance in most case as this function will reload every conflict. + * + * @param builder the builder + * @return true if was written successfully. + */ + public static boolean writeConflict(@NotNull ConflictBuilder builder) { + return writeConflict(builder, true); + } + + /** + * Write a conflict to the config file. + *

+ * You should use {@link #addConflict(ConflictBuilder)} or {@link #writeConflict(ConflictBuilder)} instead + * + * @param builder The builder + * @param updatePlanned If we should plan a global update for conflicts + * @return true if was written successfully. + */ + public static boolean writeConflict(@NotNull ConflictBuilder builder, boolean updatePlanned) { + FileConfiguration config = ConfigHolder.CONFLICT_HOLDER.getConfig(); + + String name = builder.getName(); + if (name.contains(".")) { + CustomAnvil.instance.getLogger().warning("Conflict " + name + " contain \".\" in its name but should not. this conflict is ignored."); + logConflictOrigin(builder); + return false; + } + + String basePath = name + "."; + + List enchantments = extractEnchantments(builder); + List excludedGroups = new ArrayList<>(builder.getExcludedGroupNames()); + if (!enchantments.isEmpty()) config.set(basePath + "enchantments", enchantments); + if (!excludedGroups.isEmpty()) config.set(basePath + "notAffectedGroups", excludedGroups); + if (builder.getMaxBeforeConflict() > 0) + config.set(basePath + "maxEnchantmentBeforeConflict", builder.getMaxBeforeConflict()); + + if (!config.isConfigurationSection(name)) return false; + + prepareSaveTask(); + if (updatePlanned) prepareUpdateTask(); + + return true; + } + + /** + * Extract every enchantment names from a builder. + * + * @param builder The builder storing the enchantments + * @return Builder's stored enchantment. + */ + @NotNull + private static List extractEnchantments(@NotNull ConflictBuilder builder) { + List result = new ArrayList<>(builder.getEnchantmentNames()); + for (NamespacedKey enchantmentKey : builder.getEnchantmentKeys()) { + result.add(enchantmentKey.toString()); + } + + return result; + } + + /** + * Remove a conflict. + * + * @param conflict The conflict to remove + * @return True if successful. + */ + public static boolean removeConflict(@NotNull EnchantConflictGroup conflict) { + // Remove from registry + ConfigHolder.CONFLICT_HOLDER.getConflictManager().removeConflict(conflict); + + // Delete and save to file + ConfigHolder.CONFLICT_HOLDER.delete(conflict.getName()); + prepareSaveTask(); + + // Remove from gui + EnchantConflictGui conflictGui = EnchantConflictGui.getCurrentInstance(); + if (conflictGui != null) conflictGui.removeGeneric(conflict); + + return true; + } + + /** + * Prepare a task to save conflict configuration. + */ + private static void prepareSaveTask() { + if (saveChangeTask != null) return; + + saveChangeTask = DependencyManager.scheduler.scheduleGlobally(CustomAnvil.instance, () -> { + ConfigHolder.CONFLICT_HOLDER.saveToDisk(true); + saveChangeTask = null; + }); + } + + /** + * Prepare a task to reload every conflict. + */ + private static void prepareUpdateTask() { + if (reloadChangeTask != null) return; + + reloadChangeTask = DependencyManager.scheduler.scheduleGlobally(CustomAnvil.instance, () -> { + ConfigHolder.CONFLICT_HOLDER.reload(); + EnchantConflictGui conflictGui = EnchantConflictGui.getCurrentInstance(); + if (conflictGui != null) conflictGui.reloadValues(); + + reloadChangeTask = null; + }); + } + + static void logConflictOrigin(@NotNull ConflictBuilder builder) { + CustomAnvil.instance.getLogger().warning("Conflict " + builder.getName() + " came from " + builder.getSourceName() + "."); + } + + /** + * Get every registered conflict. + * + * @return An immutable collection of conflict. + */ + @NotNull + public static List getRegisteredConflict() { + List mutableList = ConfigHolder.CONFLICT_HOLDER.getConflictManager().getConflictList(); + return Collections.unmodifiableList(mutableList); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/ConflictBuilder.java b/src/main/java/xyz/alexcrea/cuanvil/api/ConflictBuilder.java new file mode 100644 index 0000000..1460766 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/api/ConflictBuilder.java @@ -0,0 +1,459 @@ +package xyz.alexcrea.cuanvil.api; + +import io.delilaheve.CustomAnvil; +import org.bukkit.NamespacedKey; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.group.*; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +//TODO add conflict after level +/** + * A Builder for material conflict. + */ +@SuppressWarnings("unused") +public class ConflictBuilder { + + private final @Nullable Plugin source; + private @NotNull String name; + + private final @NotNull Set enchantmentNames; + private final @NotNull Set enchantmentKeys; + + private final @NotNull Set excludedGroupNames; + + private int maxBeforeConflict; + + /** + * Instantiates a new Conflict builder. + * + * @param name The conflict name + * @param maxBeforeConflict Maximum number of conflicting enchantment before conflict is active + * @param source The conflict source + */ + public ConflictBuilder(@NotNull String name, int maxBeforeConflict, @Nullable Plugin source) { + this.source = source; + this.name = name; + + this.enchantmentNames = new HashSet<>(); + this.enchantmentKeys = new HashSet<>(); + + this.excludedGroupNames = new HashSet<>(); + + this.maxBeforeConflict = maxBeforeConflict; + } + + /** + * Instantiates a new Conflict builder. + * + * @param name The conflict name + * @param source The conflict source + */ + public ConflictBuilder(@NotNull String name, @Nullable Plugin source) { + this(name, 0, source); + } + + /** + * Instantiates a new Conflict builder. + * + * @param name The conflict name + */ + public ConflictBuilder(@NotNull String name) { + this(name, null); + } + + /** + * Gets conflict source. + * + * @return The conflict source. + */ + @Nullable + public Plugin getSource() { + return source; + } + + /** + * Gets conflict source name. + * + * @return The conflict source name. + */ + @NotNull + public String getSourceName() { + if (source == null) return "an unknown source"; + + return source.getName(); + } + + /** + * Gets conflict name. + * + * @return The conflict name. + */ + @NotNull + public String getName() { + return name; + } + + /** + * Gets stored conflicting enchantment names. + * + * @return The stored enchantment names. + */ + @NotNull + public Set getEnchantmentNames() { + return enchantmentNames; + } + + /** + * Gets stored conflicting enchantment keys. + * + * @return The stored enchantment keys. + */ + @NotNull + public Set getEnchantmentKeys() { + return enchantmentKeys; + } + + /** + * Gets stored excluded group names. + * + * @return The stored group names. + */ + @NotNull + public Set getExcludedGroupNames() { + return excludedGroupNames; + } + + /** + * Gets maximum number of conflicting enchantment before conflict is active. + *

+ * This value represent how many enchantment contained on this conflict can be applied to before conflict is considered active. + * That mean new enchantment will not be able to be added to the item and present enchantment will not have its level upgraded. + *

+ * In vanilla. material restriction have this value set to 0 and enchantment conflict set to 1. + * + * @return the max number of conflicting enchantment before conflict. 0 by default. + */ + public int getMaxBeforeConflict() { + return maxBeforeConflict; + } + + /** + * Sets conflict name. + * + * @param name The name + * @return This conflict builder instance. + */ + public ConflictBuilder setName(String name) { + this.name = name; + return this; + } + + /** + * Sets maximum number of conflicting enchantment before conflict is active. + *

+ * This value represent how many enchantment contained on this conflict can be applied to before conflict is considered active. + * That mean new enchantment will not be able to be added to the item and present enchantment will not have its level upgraded. + *

+ * In vanilla. material restriction have this value set to 0 and enchantment conflict set to 1. + * + * @param maxBeforeConflict The max before conflict + * @return This conflict builder instance. + */ + public ConflictBuilder setMaxBeforeConflict(int maxBeforeConflict) { + this.maxBeforeConflict = maxBeforeConflict; + return this; + } + + /** + * Add a conflicting enchantment by name. + * + * @param enchantmentName The enchantment name + * @return This conflict builder instance. + */ + @NotNull + public ConflictBuilder addEnchantment(@NotNull String enchantmentName) { + enchantmentNames.add(enchantmentName); + return this; + } + + /** + * Add a conflicting enchantment by key. + * + * @param enchantmentKey The enchantment key + * @return This conflict builder instance. + */ + @NotNull + public ConflictBuilder addEnchantment(@NotNull NamespacedKey enchantmentKey) { + enchantmentKeys.add(enchantmentKey); + return this; + } + + /** + * Add a conflicting enchantment by instance. + * + * @param enchantment The enchantment + * @return This conflict builder instance. + */ + @NotNull + public ConflictBuilder addEnchantment(@NotNull CAEnchantment enchantment) { + addEnchantment(enchantment.getKey()); + return this; + } + + /** + * Remove conflicting enchantment by name. + * + * @param enchantmentName The enchantment name + * @return This conflict builder instance. + */ + @NotNull + public ConflictBuilder removeEnchantment(@NotNull String enchantmentName) { + enchantmentNames.remove(enchantmentName); + return this; + } + + /** + * Remove conflicting enchantment by key. + * + * @param enchantmentKey The enchantment key + * @return This conflict builder instance. + */ + @NotNull + public ConflictBuilder removeEnchantment(@NotNull NamespacedKey enchantmentKey) { + enchantmentKeys.remove(enchantmentKey); + return removeEnchantment(enchantmentKey.getKey()); + } + + /** + * Remove enchantment by instance. + * + * @param enchantment The enchantment + * @return This conflict builder instance. + */ + @NotNull + public ConflictBuilder removeEnchantment(@NotNull CAEnchantment enchantment) { + return removeEnchantment(enchantment.getKey()); + } + + /** + * Add an excluded group by name. + *

+ * If left item of an anvil craft is included on one of the excluded group it will ignore this conflict. + *

+ * This allows to create conflict only for some item. Material restriction can be written like that. + *

+ * For example: If we exclude a material group containing every pickaxe and add efficiency enchantment + * with {@link #setMaxBeforeConflict(int) maxBeforeConflict} set to 0. + * Then only pickaxe will be able to have efficiency. + * + * @param groupName The group name + * @return This conflict builder instance. + */ + @NotNull + public ConflictBuilder addExcludedGroup(@NotNull String groupName) { + excludedGroupNames.add(groupName); + return this; + } + + /** + * Add an excluded group by instance. + *

+ * If left item of an anvil craft is included on one of the excluded group it will ignore this conflict. + *

+ * This allows to create conflict only for some item. Material restriction can be written like that. + *

+ * For example: If we exclude a material group containing every pickaxe and add efficiency enchantment + * with {@link #setMaxBeforeConflict(int) maxBeforeConflict} set to 0. + * Then only pickaxe will be able to have efficiency. + * + * @param group The group + * @return this conflict builder instance. + */ + @NotNull + public ConflictBuilder addExcludedGroup(@NotNull AbstractMaterialGroup group) { + return addExcludedGroup(group.getName()); + } + + /** + * Remove an excluded group by name. + *

+ * If left item of an anvil craft is included on one of the excluded group it will ignore this conflict. + *

+ * This allows to create conflict only for some item. Material restriction can be written like that. + *

+ * For example: If we exclude a material group containing every pickaxe and add efficiency enchantment + * with {@link #setMaxBeforeConflict(int) maxBeforeConflict} set to 0. + * Then only pickaxe will be able to have efficiency. + * + * @param groupName The group name + * @return This conflict builder instance. + */ + @NotNull + public ConflictBuilder removeExcludedGroup(@NotNull String groupName) { + excludedGroupNames.remove(groupName); + return this; + } + + /** + * Remove an excluded group by instance. + *

+ * If left item of an anvil craft is included on one of the excluded group it will ignore this conflict. + *

+ * This allows to create conflict only for some item. Material restriction can be written like that. + *

+ * For example: If we exclude a material group containing every pickaxe and add efficiency enchantment + * with {@link #setMaxBeforeConflict(int) maxBeforeConflict} set to 0. + * Then only pickaxe will be able to have efficiency. + * + * @param group The group + * @return This conflict builder instance. + */ + @NotNull + public ConflictBuilder removeExcludedGroup(@NotNull AbstractMaterialGroup group) { + return removeExcludedGroup(group.getName()); + } + + /** + * Copy this conflict builder. + * + * @return A copy of this conflict builder. + */ + @NotNull + public ConflictBuilder copy() { + ConflictBuilder copy = new ConflictBuilder(this.name, this.source); + + copy.setMaxBeforeConflict(this.maxBeforeConflict); + + // Set Enchantments + for (NamespacedKey key : this.enchantmentKeys) { + copy.addEnchantment(key); + } + for (String enchantName : this.enchantmentNames) { + copy.addEnchantment(enchantName); + } + + // Set Groups + for (String groupName : this.excludedGroupNames) { + copy.addExcludedGroup(groupName); + } + + return copy; + } + + /** + * Build a new Enchant conflict group by this builder. + * + * @return An Enchant conflict group with this builder parameters. + */ + public EnchantConflictGroup build() { + AbstractMaterialGroup materials = extractGroups(); + EnchantConflictGroup conflict = new EnchantConflictGroup(getName(), materials, getMaxBeforeConflict()); + appendEnchantments(conflict); + + return conflict; + } + + /** + * Register this conflict if not yet registered. + * Equivalent to {@link ConflictAPI#addConflict(ConflictBuilder, boolean) ConflictAPI.addConflict(this, true)}} + * + * @return True if successful. + */ + public boolean registerIfAbsent() { + return ConflictAPI.addConflict(this, true); + } + + /** + * Register this conflict if not yet registered or deleted. + * Equivalent to {@link ConflictAPI#addConflict(ConflictBuilder) ConflictAPI.addConflict(this)} + * + * @return True if successful. + */ + public boolean registerIfNew() { + return ConflictAPI.addConflict(this); + } + + /** + * Append builders stored enchantments into conflict. + * + * @param conflict The conflict target + */ + protected void appendEnchantments(@NotNull EnchantConflictGroup conflict) { + for (String enchantmentName : getEnchantmentNames()) { + if (appendEnchantments(conflict, EnchantmentApi.getListByName(enchantmentName)) == 0) { + CustomAnvil.instance.getLogger().warning("Could not find enchantment " + enchantmentName + " for conflict " + getName()); + ConflictAPI.logConflictOrigin(this); + } + } + for (NamespacedKey enchantmentKey : getEnchantmentKeys()) { + if (!appendEnchantment(conflict, EnchantmentApi.getByKey(enchantmentKey))) { + CustomAnvil.instance.getLogger().warning("Could not find enchantment " + enchantmentKey + " for conflict " + getName()); + ConflictAPI.logConflictOrigin(this); + } + } + } + + /** + * Append an enchantment. + * + * @param conflict The conflict target + * @param enchantment The enchantment + * @return True if successful. + */ + protected static boolean appendEnchantment(@NotNull EnchantConflictGroup conflict, @Nullable CAEnchantment enchantment) { + if (enchantment == null) + return false; + conflict.addEnchantment(enchantment); + return true; + } + + /** + * Append a list of enchantments. + * + * @param conflict The conflict target + * @param enchantments List of enchantment to add + * @return Number of enchantment added + */ + protected static int appendEnchantments(@NotNull EnchantConflictGroup conflict, @NotNull List enchantments) { + int numberValid = 0; + for (CAEnchantment enchantment : enchantments) { + if (appendEnchantment(conflict, enchantment)) { + numberValid++; + } + } + + return numberValid; + } + + /** + * Extract group abstract material group. + * + * @return The abstract material group from the builder. + */ + protected AbstractMaterialGroup extractGroups() { + ItemGroupManager itemGroupManager = ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager(); + IncludeGroup group = new IncludeGroup(EnchantConflictManager.DEFAULT_GROUP_NAME); + + for (String groupName : getExcludedGroupNames()) { + AbstractMaterialGroup materialGroup = itemGroupManager.get(groupName); + + if (materialGroup == null) { + CustomAnvil.instance.getLogger().warning("Material group " + groupName + " do not exist but is ask by conflict " + getName()); + ConflictAPI.logConflictOrigin(this); + continue; + } + + group.addToPolicy(materialGroup); + } + + return group; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/CustomAnvilRecipeApi.java b/src/main/java/xyz/alexcrea/cuanvil/api/CustomAnvilRecipeApi.java new file mode 100644 index 0000000..8f80aa3 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/api/CustomAnvilRecipeApi.java @@ -0,0 +1,126 @@ +package xyz.alexcrea.cuanvil.api; + +import io.delilaheve.CustomAnvil; +import org.bukkit.configuration.file.FileConfiguration; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.dependency.DependencyManager; +import xyz.alexcrea.cuanvil.gui.config.global.CustomRecipeConfigGui; +import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe; + +import java.util.Collections; +import java.util.List; + +/** + * Custom Anvil api for custom anvil recipes. + */ +@SuppressWarnings("unused") +public class CustomAnvilRecipeApi { + + private CustomAnvilRecipeApi(){} + + private static Object saveChangeTask = null; + + /** + * Write and add a custom anvil recipe. + * Will not write the recipe if it already exists. + * + * @param builder The recipe builder to be based on + * @return True if successful. + */ + public static boolean addRecipe(@NotNull AnvilRecipeBuilder builder){ + return addRecipe(builder, false); + } + + /** + * Write and add a custom anvil recipe. + * Will not write the recipe if it already exists. + * + * @param builder The recipe builder to be based on + * @param overrideDeleted If we should write even if the recipe was previously deleted. + * @return True if successful. + */ + public static boolean addRecipe(@NotNull AnvilRecipeBuilder builder, boolean overrideDeleted){ + FileConfiguration config = ConfigHolder.CUSTOM_RECIPE_HOLDER.getConfig(); + String name = builder.getName(); + + if(!overrideDeleted && ConfigHolder.CUSTOM_RECIPE_HOLDER.isDeleted(builder.getName())) return false; + if(config.contains(builder.getName())) return false; + + if(builder.getName().contains(".")) { + CustomAnvil.instance.getLogger().warning("Custom anvil recipe " + name + " contain \".\" in its name but should not. this recipe is ignored."); + return false; + } + + AnvilCustomRecipe recipe = builder.build(); + if(recipe == null){ + CustomAnvil.instance.getLogger().warning("Custom anvil recipe " + name + " could not be parsed."); + if(builder.getLeftItem() == null){ + CustomAnvil.instance.getLogger().warning("It look like left item of the recipe is null."); + } + if(builder.getResultItem() == null){ + CustomAnvil.instance.getLogger().warning("It look like result item of the recipe is null."); + } + return false; + } + + // Add to registry + ConfigHolder.CUSTOM_RECIPE_HOLDER.getRecipeManager().cleanAddNew(recipe); + + // Save to file + recipe.saveToFile(false, false); + prepareSaveTask(); + + // Add from gui + CustomRecipeConfigGui recipeConfigGui = CustomRecipeConfigGui.getCurrentInstance(); + if(recipeConfigGui != null) recipeConfigGui.updateValueForGeneric(recipe, true); + + return true; + } + + // TODO remove by name and/or by builder (as name is keept) (and maybe create a get by name) + /** + * Remove a custom anvil recipe. + * + * @param recipe The recipe to remove + * @return True if successful. + */ + public static boolean removeRecipe(@NotNull AnvilCustomRecipe recipe){ + // Remove from registry + boolean result = ConfigHolder.CUSTOM_RECIPE_HOLDER.getRecipeManager().cleanRemove(recipe); + if(!result) return false; + + // Delete and save to file + ConfigHolder.CUSTOM_RECIPE_HOLDER.delete(recipe.getName()); + prepareSaveTask(); + + // Remove from gui + CustomRecipeConfigGui recipeConfigGui = CustomRecipeConfigGui.getCurrentInstance(); + if(recipeConfigGui != null) recipeConfigGui.removeGeneric(recipe); + + return true; + } + + /** + * Prepare a task to save custom recipe configuration. + */ + private static void prepareSaveTask() { + if(saveChangeTask != null) return; + + saveChangeTask = DependencyManager.scheduler.scheduleGlobally(CustomAnvil.instance, ()->{ + ConfigHolder.CONFLICT_HOLDER.saveToDisk(true); + saveChangeTask = null; + }); + } + + /** + * Get every registered recipes. + * @return An immutable collection of recipes. + */ + @NotNull + public static List getRegisteredRecipes(){ + List mutableList = ConfigHolder.CUSTOM_RECIPE_HOLDER.getRecipeManager().getRecipeList(); + return Collections.unmodifiableList(mutableList); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/EnchantmentApi.java b/src/main/java/xyz/alexcrea/cuanvil/api/EnchantmentApi.java new file mode 100644 index 0000000..ac98225 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/api/EnchantmentApi.java @@ -0,0 +1,235 @@ +package xyz.alexcrea.cuanvil.api; + +import io.delilaheve.CustomAnvil; +import io.delilaheve.util.ConfigOptions; +import org.bukkit.NamespacedKey; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.enchantments.Enchantment; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.dependency.DependencyManager; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry; +import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity; +import xyz.alexcrea.cuanvil.enchant.bulk.BulkCleanEnchantOperation; +import xyz.alexcrea.cuanvil.enchant.bulk.BulkGetEnchantOperation; +import xyz.alexcrea.cuanvil.enchant.wrapped.CABukkitEnchantment; +import xyz.alexcrea.cuanvil.gui.config.global.EnchantCostConfigGui; +import xyz.alexcrea.cuanvil.gui.config.global.EnchantLimitConfigGui; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Custom Anvil api for enchantment registry. + */ +@SuppressWarnings("unused") +public class EnchantmentApi { + + private static Object saveChangeTask = null; + + private EnchantmentApi() {} + + /** + * Register an enchantment. + * + * @param enchantment The enchantment to register + * @return True if successful. + */ + public static boolean registerEnchantment(@NotNull CAEnchantment enchantment){ + if(!CAEnchantmentRegistry.getInstance().register(enchantment)) return false; + + // Add enchantment to gui. + if(EnchantCostConfigGui.getInstance() != null){ + EnchantCostConfigGui.getInstance().updateValueForGeneric(enchantment, true); + } + if(EnchantLimitConfigGui.getInstance() != null){ + EnchantLimitConfigGui.getInstance().updateValueForGeneric(enchantment, true); + } + + // Write default if do not exist + writeDefaultConfig(enchantment, false); + + return true; + } + + /** + * Register an enchantment by minecraft registered enchantment instance. + * + * @param enchantment The enchantment to register + * @param defaultRarity The default rarity of the provided enchantment + * @return True if successful. + */ + public static boolean registerEnchantment(@NotNull Enchantment enchantment, @Nullable EnchantmentRarity defaultRarity){ + if(defaultRarity == null) + return registerEnchantment(new CABukkitEnchantment(enchantment)); + + return registerEnchantment(new CABukkitEnchantment(enchantment, defaultRarity)); + } + + /** + * Register an enchantment by minecraft registered enchantment instance. + *

+ * Please note that this function assume the provided enchantment is registered into minecraft registry. + * + * @param enchantment The enchantment to register + * @return True if successful. + */ + public static boolean registerEnchantment(@NotNull Enchantment enchantment){ + return registerEnchantment(new CABukkitEnchantment(enchantment)); + } + + /** + * Unregister an enchantment. + * + * @param enchantment The enchantment to unregister + * @return True if successful. + */ + public static boolean unregisterEnchantment(@Nullable CAEnchantment enchantment){ + // Remove from gui + if(EnchantCostConfigGui.getInstance() != null){ + EnchantCostConfigGui.getInstance().removeGeneric(enchantment); + } + if(EnchantLimitConfigGui.getInstance() != null){ + EnchantLimitConfigGui.getInstance().removeGeneric(enchantment); + } + + return CAEnchantmentRegistry.getInstance().unregister(enchantment); + } + + /** + * Unregister an enchantment by its key. + * + * @param key The enchantment key to unregister + * @return True if successful. + */ + public static boolean unregisterEnchantment(@NotNull NamespacedKey key){ + CAEnchantment enchantment = CAEnchantment.getByKey(key); + return unregisterEnchantment(enchantment); + } + + /** + * Unregister an enchantment by his bukkit enchantment. + * + * @param enchantment The enchantment to unregister + * @return True if successful. + */ + public static boolean unregisterEnchantment(@NotNull Enchantment enchantment){ + return unregisterEnchantment(enchantment.getKey()); + } + + /** + * Get by key an enchantment. + * + * @param key The key used to fetch + * @return The custom anvil enchantment of this key. null if not found. + */ + @Nullable + public static CAEnchantment getByKey(@NotNull NamespacedKey key){ + return CAEnchantment.getByKey(key); + } + + /** + * Get by name an enchantment. + * + * @param name The name used to fetch + * @return The custom anvil enchantment of this name. null if not found. + * @deprecated use {@link #getListByName(String)} + */ + @Deprecated(since = "1.6.3") + @Nullable + public static CAEnchantment getByName(@NotNull String name){ + return CAEnchantment.getByName(name); + } + + /** + * Get list of enchantment using the provided name. + * + * @param name The name used to fetch + * @return List of custom anvil enchantments of this name. May be empty if not found. + */ + public static List getListByName(@NotNull String name){ + return CAEnchantment.getListByName(name); + } + + /** + * Get every registered custom anvil enchantments. + * @return An immutable map of enchantment key as map key and custom anvil enchantment as value. + */ + @NotNull + public static Map getRegisteredEnchantments(){ + return Collections.unmodifiableMap(CAEnchantmentRegistry.getInstance().registeredEnchantments()); + } + + /** + * Write the default level and rarity configuration of the enchantment. + * @param enchantment The enchantment to write default configuration + * @param override If it should override old configuration + * @return Return false if override is false and a configuration exist. true otherwise. + */ + public static boolean writeDefaultConfig(CAEnchantment enchantment, boolean override){ + FileConfiguration config = ConfigHolder.DEFAULT_CONFIG.getConfig(); + + if(tryWriteDefaultConfig(config, enchantment, override)){ + prepareSaveTask(); + } + return true; + } + + private static boolean tryWriteDefaultConfig(FileConfiguration defaultConfig, CAEnchantment enchantment, boolean override) { + boolean hasChange = false; + + String levelPath = ConfigOptions.ENCHANT_LIMIT_ROOT + "." + enchantment.getKey(); + if(override || !defaultConfig.isSet(levelPath)){ + defaultConfig.set(levelPath, enchantment.defaultMaxLevel()); + hasChange = true; + } + + String basePath = ConfigOptions.ENCHANT_VALUES_ROOT + "." + enchantment.getKey(); + EnchantmentRarity rarity = enchantment.defaultRarity(); + + String itemPath = basePath + ".item"; + String bookPath = basePath + ".book"; + if(override || !defaultConfig.isSet(itemPath)){ + defaultConfig.set(itemPath, rarity.getItemValue()); + hasChange = true; + } + if(override || !defaultConfig.isSet(bookPath)){ + defaultConfig.set(bookPath, rarity.getBookValue()); + hasChange = true; + } + + return hasChange; + } + + /** + * Prepare a task to save custom recipe configuration. + */ + private static void prepareSaveTask() { + if(saveChangeTask != null) return; + + saveChangeTask = DependencyManager.scheduler.scheduleGlobally(CustomAnvil.instance, ()->{ + ConfigHolder.DEFAULT_CONFIG.saveToDisk(true); + saveChangeTask = null; + }); + } + + /** + * Add a bulk get operator. + * @param operation An optimised get enchantments operation + */ + public static void addBulkGet(@NotNull BulkGetEnchantOperation operation){ + CAEnchantmentRegistry.getInstance().getOptimisedGetOperators().add(operation); + } + + /** + * Add a bulk clean operator. + * @param operation An optimised clean enchantments operation + */ + public static void addBulkClean(@NotNull BulkCleanEnchantOperation operation){ + CAEnchantmentRegistry.getInstance().getOptimisedCleanOperators().add(operation); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/MaterialGroupApi.java b/src/main/java/xyz/alexcrea/cuanvil/api/MaterialGroupApi.java new file mode 100644 index 0000000..cd71c7a --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/api/MaterialGroupApi.java @@ -0,0 +1,251 @@ +package xyz.alexcrea.cuanvil.api; + +import io.delilaheve.CustomAnvil; +import io.delilaheve.util.ConfigOptions; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.configuration.file.FileConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.dependency.DependencyManager; +import xyz.alexcrea.cuanvil.group.AbstractMaterialGroup; +import xyz.alexcrea.cuanvil.group.ExcludeGroup; +import xyz.alexcrea.cuanvil.group.IncludeGroup; +import xyz.alexcrea.cuanvil.group.ItemGroupManager; +import xyz.alexcrea.cuanvil.gui.config.global.GroupConfigGui; + +import java.util.*; + +/** + * Custom Anvil api for material group registry. + */ +@SuppressWarnings("unused") +public class MaterialGroupApi { + + private MaterialGroupApi() { + } + + private static Object saveChangeTask = null; + private static Object reloadChangeTask = null; + + /** + * Write and add a group. + * Will not write the group if it already exists. + * Will not be successful if the group is empty. + * + * @param group The group to add + * @return true if successful. + */ + public static boolean addMaterialGroup(@NotNull AbstractMaterialGroup group) { + return addMaterialGroup(group, false); + } + + /** + * Write and add a group. + * Will not write the group if it already exists. + * Will not be successful if the group is empty. + * + * @param group The group to add + * @param overrideDeleted If we should write even if the group was previously deleted. + * @return true if successful. + */ + public static boolean addMaterialGroup(@NotNull AbstractMaterialGroup group, boolean overrideDeleted) { + ItemGroupManager itemGroupManager = ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager(); + + // Test if it exists/existed + if (!overrideDeleted && ConfigHolder.ITEM_GROUP_HOLDER.isDeleted(group.getName())) return false; + if (itemGroupManager.get(group.getName()) != null) return false; + + // Add group + itemGroupManager.getGroupMap().put(group.getName(), group); + + if (!writeMaterialGroup(group, false)) return false; + + if (group instanceof IncludeGroup includeGroup) { + GroupConfigGui configGui = GroupConfigGui.getCurrentInstance(); + if (configGui != null) configGui.updateValueForGeneric(includeGroup, true); + } + + if (ConfigOptions.INSTANCE.getVerboseDebugLog()) { + CustomAnvil.instance.getLogger().info("Registered group " + group.getName()); + } + + return true; + } + + /** + * Write a material group to the config file and plan an update of groups. + *

+ * You may want to use {@link #addMaterialGroup(AbstractMaterialGroup)} instead as it is more performance in most case as this function will reload every conflict. + * + * @param group the group to write + * @return true if was written successfully. + */ + public static boolean writeMaterialGroup(@NotNull AbstractMaterialGroup group) { + return writeMaterialGroup(group, true); + } + + /** + * Write a material group to the config file. + *

+ * You should use {@link #addMaterialGroup(AbstractMaterialGroup)} or {@link #writeMaterialGroup(AbstractMaterialGroup)} instead + * + * @param group the group to write + * @param updatePlanned if we should plan a global update for material groups + * @return true if was written successfully. + */ + public static boolean writeMaterialGroup(@NotNull AbstractMaterialGroup group, boolean updatePlanned) { + String name = group.getName(); + if (name.contains(".")) { + CustomAnvil.instance.getLogger().warning("Group " + name + " contain . in its name but should not. this material group is ignored."); + return false; + } + + boolean changed; + if (group instanceof IncludeGroup includeGroup) { + changed = writeKnownGroup("include", includeGroup); + } else if (group instanceof ExcludeGroup excludeGroup) { + throw new UnsupportedOperationException("exclude group is temporarily disable for the time being. sorry"); + // This code do not do what is intended ? idk why do it exist + //changed = writeKnownGroup("exclude", excludeGroup); + } else { + changed = writeUnknownGroup(group); + } + if (!changed) return false; + + prepareSaveTask(); + if (updatePlanned) prepareUpdateTask(); + + return true; + } + + private static boolean writeKnownGroup(@NotNull String groupType, @NotNull AbstractMaterialGroup group) { + FileConfiguration config = ConfigHolder.ITEM_GROUP_HOLDER.getConfig(); + + String basePath = group.getName() + "."; + Set materialSet = group.getNonGroupInheritedMaterials(); + Set groupSet = group.getGroups(); + + boolean empty = true; + if (!materialSet.isEmpty()) { + config.set(basePath + ItemGroupManager.MATERIAL_LIST_PATH, materialSetToStringList(materialSet)); + empty = false; + } else { + config.set(basePath + ItemGroupManager.MATERIAL_LIST_PATH, null); + } + if (!groupSet.isEmpty()) { + config.set(basePath + ItemGroupManager.GROUP_LIST_PATH, materialGroupSetToStringList(groupSet)); + empty = false; + } else { + config.set(basePath + ItemGroupManager.GROUP_LIST_PATH, null); + } + + if (empty) { + config.set(basePath + ItemGroupManager.GROUP_TYPE_PATH, null); + return false; + } + + config.set(basePath + ItemGroupManager.GROUP_TYPE_PATH, groupType); + return true; + } + + private static boolean writeUnknownGroup(@NotNull AbstractMaterialGroup group) { + FileConfiguration config = ConfigHolder.ITEM_GROUP_HOLDER.getConfig(); + + String basePath = group.getName() + "."; + Set materials = group.getMaterials(); + + if (materials.isEmpty()) return false; + + config.set(basePath + ItemGroupManager.GROUP_TYPE_PATH, "include"); + config.set(basePath + ItemGroupManager.MATERIAL_LIST_PATH, materialSetToStringList(materials)); + + return true; + } + + public static List materialSetToStringList(@NotNull Set materials) { + return materials.stream().map(NamespacedKey::toString).toList(); + } + + public static List materialGroupSetToStringList(@NotNull Set groups) { + return groups.stream().map(AbstractMaterialGroup::getName).toList(); + } + + /** + * Remove a material group. + * Caution ! It will not be removed from depending conflict or other material group at runtime. + * For that reason, it is not recommended to use this function. + * + * @param group The recipe to remove + * @return True if the group was present. + */ + public static boolean removeGroup(@NotNull AbstractMaterialGroup group) { + // Remove from registry + AbstractMaterialGroup removed = ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager().groupMap.remove(group.getName()); + if (removed == null) return false; + + // Delete and save to file + ConfigHolder.ITEM_GROUP_HOLDER.delete(group.getName()); + prepareSaveTask(); + + // Remove from gui + if (group instanceof IncludeGroup includeGroup) { + GroupConfigGui configGui = GroupConfigGui.getCurrentInstance(); + if (configGui != null) configGui.removeGeneric(includeGroup); + } + + return true; + } + + /** + * Prepare a task to reload every conflict. + */ + private static void prepareSaveTask() { + if (saveChangeTask != null) return; + + saveChangeTask = DependencyManager.scheduler.scheduleGlobally(CustomAnvil.instance, () -> { + ConfigHolder.ITEM_GROUP_HOLDER.saveToDisk(true); + saveChangeTask = null; + }); + } + + /** + * Prepare a task to save configuration. + */ + private static void prepareUpdateTask() { + if (reloadChangeTask != null) return; + + reloadChangeTask = DependencyManager.scheduler.scheduleGlobally(CustomAnvil.instance, () -> { + ConfigHolder.ITEM_GROUP_HOLDER.reload(); + + GroupConfigGui configGui = GroupConfigGui.getCurrentInstance(); + if (configGui != null) configGui.reloadValues(); + + reloadChangeTask = null; + }); + + } + + /** + * Get by name a group. + * + * @param groupName the group name used to fetch + * @return the abstract group of this name. null if not found. + */ + @Nullable + public static AbstractMaterialGroup getGroup(@NotNull String groupName) { + return ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager().get(groupName); + } + + /** + * Get every registered material groups. + * + * @return An immutable map of group name as its key and group as mapped value. + */ + @NotNull + public static Map getRegisteredGroups() { + return Collections.unmodifiableMap(ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager().getGroupMap()); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/UnitRepairApi.java b/src/main/java/xyz/alexcrea/cuanvil/api/UnitRepairApi.java new file mode 100644 index 0000000..bc50c16 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/api/UnitRepairApi.java @@ -0,0 +1,218 @@ +package xyz.alexcrea.cuanvil.api; + +import io.delilaheve.CustomAnvil; +import kotlin.Triple; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.dependency.DependencyManager; +import xyz.alexcrea.cuanvil.gui.config.global.UnitRepairConfigGui; +import xyz.alexcrea.cuanvil.gui.config.list.MappedGuiListConfigGui; +import xyz.alexcrea.cuanvil.gui.config.list.UnitRepairElementListGui; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Custom Anvil api for unit repair. + */ +@SuppressWarnings("unused") +public class UnitRepairApi { + + private UnitRepairApi(){} + + private static Object saveChangeTask = null; + + /** + * Write and add a custom anvil unit repair recipe. + * Will not write the recipe if it already exists or was deleted. + * Set the value to minecraft default value (0.25 = 25%) + * + * @param unit The unit material used to repair the bellow item. + * @param repairable The item to be repaired. + * @return true if successful. + */ + public static boolean addUnitRepair(@NotNull Material unit, @NotNull Material repairable){ + return addUnitRepair(unit, repairable, 0.25, false); + } + + /** + * Write and add a custom anvil unit repair recipe. + * Will not write the recipe if it already exists or was deleted. + * + * @param unit The unit material used to repair the bellow item. + * @param repairable The item to be repaired. + * @param value The amount to be repaired by every unit. (1% = 0.01) + * @return true if successful. + */ + public static boolean addUnitRepair(@NotNull Material unit, @NotNull Material repairable, double value){ + return addUnitRepair(unit, repairable, value, false); + } + + /** + * Write and add a custom anvil unit repair recipe. + * Will not write the recipe if it already exists. + * + * @param unit The unit material used to repair the bellow item. + * @param repairable The item to be repaired. + * @param value The amount to be repaired by every unit. (1% = 0.01) + * @param overrideDeleted If we should write even if the recipe was previously deleted. + * @return true if successful. + */ + public static boolean addUnitRepair(@NotNull Material unit, @NotNull Material repairable, double value, boolean overrideDeleted){ + FileConfiguration config = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig(); + String path = unit.name().toLowerCase() + "." + repairable.name().toLowerCase(); + + if(!overrideDeleted && ConfigHolder.UNIT_REPAIR_HOLDER.isDeleted(path)) return false; + if(config.contains(path)) return false; + + // Set unit repair + return setUnitRepair(unit, repairable, value); + } + + /** + * Write and add a custom anvil unit repair recipe. + * Do not check if it previously existed or exist. + * + * @param unit The unit material used to repair the bellow item. + * @param repairable The item to be repaired. + * @param value The amount to be repaired by every unit. (1% = 0.01) + * @return true if successful. + */ + public static boolean setUnitRepair(@NotNull Material unit, @NotNull Material repairable, double value){ + FileConfiguration config = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig(); + + String repairableName = repairable.name().toLowerCase(); + String path = unit.name().toLowerCase() + "." + repairableName; + + // Add to config then prepare save + config.set(path, value); + prepareSaveTask(); + + // Add to gui + UnitRepairConfigGui repairConfigGui = UnitRepairConfigGui.getCurrentInstance(); + if(repairConfigGui != null) { + UnitRepairElementListGui elementGui = repairConfigGui.getInstanceOrCreate(unit).getStored(); + + if(elementGui != null) elementGui.updateValueForGeneric(repairableName, true); + repairConfigGui.updateValueForGeneric(unit, true); + } + + return true; + } + + /** + * Remove a custom anvil unit repair recipe. + * + * @param unit The unit material used to repair the bellow item. + * @param repairable The item used to be repaired. + * @return true if successful. + */ + public static boolean removeUnitRepair(@NotNull Material unit, @NotNull Material repairable){ + // Delete every possible variation and save to file + String unitName = unit.name(); + String repairableName = repairable.name(); + + FileConfiguration config = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig(); + config.set(unitName.toLowerCase() + "." + repairableName.toUpperCase(), null); + config.set(unitName.toUpperCase() + "." + repairableName.toLowerCase(), null); + config.set(unitName.toUpperCase() + "." + repairableName.toUpperCase(), null); + config.set(unitName.toLowerCase() + "." + repairableName.toLowerCase(), null); + + // Test if it was the last value of this section + boolean lastValue = false; + if(config.isConfigurationSection(unitName.toLowerCase())) { + ConfigurationSection section = config.getConfigurationSection(unitName.toLowerCase()); + + if(section != null && section.getKeys(false).isEmpty()) { + lastValue = true; + config.set(unitName.toLowerCase(), null); + } + + } else if (config.isConfigurationSection(unitName.toUpperCase())) { + ConfigurationSection section = config.getConfigurationSection(unitName.toUpperCase()); + if(section != null && section.getKeys(false).isEmpty()) { + lastValue = true; + config.set(unitName.toUpperCase(), null); + } + + } else lastValue = true; + + + // We only need to "delete" as the lower case to be counted as deleted + ConfigHolder.UNIT_REPAIR_HOLDER.delete(unitName.toLowerCase() + "." + repairableName.toLowerCase()); + prepareSaveTask(); + + // Remove from gui + UnitRepairConfigGui repairConfigGui = UnitRepairConfigGui.getCurrentInstance(); + if(repairConfigGui != null) { + UnitRepairElementListGui elementGui = repairConfigGui.getInstanceOrCreate(unit).getStored(); + + if(elementGui != null) elementGui.removeGeneric(repairableName); + if(lastValue){ + repairConfigGui.removeGeneric(unit); + } + } + + return true; + } + + /** + * Prepare a task to save custom unit repair recipe configuration. + */ + private static void prepareSaveTask() { + if(saveChangeTask != null) return; + + saveChangeTask = DependencyManager.scheduler.scheduleGlobally(CustomAnvil.instance, ()->{ + ConfigHolder.UNIT_REPAIR_HOLDER.saveToDisk(true); + saveChangeTask = null; + }); + } + + /** + * Get every unit repair recipes. + * @return An immutable collection of unit repair recipes. + *

+ * Each element of the provided triple represent a part of the recipe + *

    + *
  • First object is the unit material used to repair the bellow item. + *
  • Second object is the item to be repaired. + *
  • Last object is the amount to be repaired by every unit. (1% = 0.01) + *
+ */ + @NotNull + public static List> getUnitRepairs(){ + List> mutableList = new ArrayList<>(); + + FileConfiguration config = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig(); + for (String unitKey : config.getKeys(false)) { + // Test if config section exist + if(!config.isConfigurationSection(unitKey)) continue; + + // Test if unit is a material + Material unit = Material.getMaterial(unitKey.toUpperCase()); + if(unit == null) continue; + + // Iterate over reparable items + ConfigurationSection section = config.getConfigurationSection(unitKey); + for (String repairableKey : section.getKeys(false)) { + // Test if value section exist + if(!section.isDouble(repairableKey)) continue; + + // Test if repairable is valid a material + Material repairable = Material.getMaterial(repairableKey.toUpperCase()); + if(repairable == null) continue; + + // Add the values + mutableList.add(new Triple<>(unit, repairable, section.getDouble(repairableKey))); + + } + } + + return Collections.unmodifiableList(mutableList); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/event/CAConfigReadyEvent.java b/src/main/java/xyz/alexcrea/cuanvil/api/event/CAConfigReadyEvent.java new file mode 100644 index 0000000..67d27a8 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/CAConfigReadyEvent.java @@ -0,0 +1,36 @@ +package xyz.alexcrea.cuanvil.api.event; + +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * Called when the configuration of CustomAnvil is ready. + * It is called either on the plugin startup or on the plugin config reload. + *

+ * If you want to listen to the first trigger of this event (first configuration load. aka plugin load) + * you will need to register the listener on your plugin onEnable or earlier + *

+ * This event indicate that can start to register your recipes, item groups and conflicts. + * The vanilla and custom enchantments should already have been provided to CustomAnvil. + * Configuration can be changed any time after this event is triggered but never before. + *

+ * use {@link xyz.alexcrea.cuanvil.api.ConflictAPI ConflictApi}, + * {@link xyz.alexcrea.cuanvil.gui.config.global.CustomRecipeConfigGui CustomRecipeConfigGui}, + * {@link xyz.alexcrea.cuanvil.api.MaterialGroupApi MaterialGroupApi} + * and {@link xyz.alexcrea.cuanvil.api.UnitRepairApi UnitRepairApi} + * to add/remove/edit configurations + */ +public class CAConfigReadyEvent extends Event { + + private static final HandlerList HANDLERS = new HandlerList(); + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public HandlerList getHandlers() { + return HANDLERS; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/event/CAEnchantRegistryReadyEvent.java b/src/main/java/xyz/alexcrea/cuanvil/api/event/CAEnchantRegistryReadyEvent.java new file mode 100644 index 0000000..3ffe372 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/CAEnchantRegistryReadyEvent.java @@ -0,0 +1,29 @@ +package xyz.alexcrea.cuanvil.api.event; + +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * Called when custom anvil is ready to accept registration on custom enchantment. + *

+ * If you want to listen this event + * you will need to register the listener on your plugin onEnable or earlier + *

+ * Custom enchantments may be registered later but may cause issue if registered too later + * (after configuration loading phase. see {@link CAConfigReadyEvent}) + *

+ * use {@link xyz.alexcrea.cuanvil.api.EnchantmentApi EnchantmentApi} to register and unregister your custom enchantments + */ +public class CAEnchantRegistryReadyEvent extends Event { + + private static final HandlerList HANDLERS = new HandlerList(); + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public HandlerList getHandlers() { + return HANDLERS; + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAClickResultBypassEvent.java b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAClickResultBypassEvent.java new file mode 100644 index 0000000..fe5e199 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAClickResultBypassEvent.java @@ -0,0 +1,63 @@ +package xyz.alexcrea.cuanvil.api.event.listener; + +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.jetbrains.annotations.NotNull; + +/** + * Called before custom anvil process the click on the result on the anvil inventory. + *

+ * This event is called after checking that the inventory is an anvil inventory and that the click is on the result slot + * but before checking if the player has the custom anvil affected permission. + *

+ * This event being cancelled will make CustomAnvil abort the click on result process. + *

+ * Most of the time you would likely need {@link CAPreAnvilBypassEvent} or {@link CAEarlyPreAnvilBypassEvent} + * for this event to be useful. + *

+ * There is also {@link CATreatAnvilResult2Event} that may be better for some use case. + */ +public class CAClickResultBypassEvent extends Event implements Cancellable { + + private static final HandlerList HANDLERS = new HandlerList(); + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + private boolean cancelled = false; + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @NotNull + private final InventoryClickEvent event; + + /** + * Get the bukkit inventory click event causing to this event + * + * @return The click event causing to this event + */ + @NotNull + public InventoryClickEvent getEvent() { + return event; + } + + public CAClickResultBypassEvent(@NotNull InventoryClickEvent event) { + this.event = event; + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAEarlyPreAnvilBypassEvent.java b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAEarlyPreAnvilBypassEvent.java new file mode 100644 index 0000000..e92b4cd --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAEarlyPreAnvilBypassEvent.java @@ -0,0 +1,63 @@ +package xyz.alexcrea.cuanvil.api.event.listener; + +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.bukkit.event.inventory.PrepareAnvilEvent; +import org.jetbrains.annotations.NotNull; + +/** + * Called before custom anvil process the prepare anvil event. + *

+ * This event will always get called when CustomAnvil need to handle + *

+ * This event being cancelled will make CustomAnvil abort the anvil process. + *

+ * You should also use {@link CAClickResultBypassEvent} if you want to use this event for something useful. + *

+ * It is also recommended that you read about {@link CAPreAnvilBypassEvent} and {@link CATreatAnvilResult2Event} + * as your use case may be more prone to use theses. + */ +public class CAEarlyPreAnvilBypassEvent extends Event implements Cancellable { + + private static final HandlerList HANDLERS = new HandlerList(); + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + private boolean cancelled = false; + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @NotNull + private final PrepareAnvilEvent event; + + /** + * Get the bukkit pre anvil event causing this event + * + * @return The pre anvil event causing to this event + */ + @NotNull + public PrepareAnvilEvent getEvent() { + return event; + } + + public CAEarlyPreAnvilBypassEvent(@NotNull PrepareAnvilEvent event) { + this.event = event; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAPreAnvilBypassEvent.java b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAPreAnvilBypassEvent.java new file mode 100644 index 0000000..9103a4b --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAPreAnvilBypassEvent.java @@ -0,0 +1,66 @@ +package xyz.alexcrea.cuanvil.api.event.listener; + +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.bukkit.event.inventory.PrepareAnvilEvent; +import org.jetbrains.annotations.NotNull; + +/** + * Called before custom anvil process the prepare anvil event. + *

+ * This event is called after {@link CAEarlyPreAnvilBypassEvent}, + * after checking that there is at least an item on the left slot + * and after checking if any of the 2 item is marked as immutable + * but before checking if the player has the custom anvil affected permission. + *

+ * This event being cancelled will make CustomAnvil abort the anvil process. + *

+ * You should also use {@link CAClickResultBypassEvent} if you want to use this event for something useful. + *

+ * It is also recommended that you read about {@link CAEarlyPreAnvilBypassEvent} and {@link CATreatAnvilResult2Event} + * as your use case may be more prone to use theses. + */ +public class CAPreAnvilBypassEvent extends Event implements Cancellable { + + private static final HandlerList HANDLERS = new HandlerList(); + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + private boolean cancelled = false; + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @NotNull + private final PrepareAnvilEvent event; + + /** + * Get the bukkit pre anvil event causing this event + * + * @return The pre anvil event causing this event + */ + @NotNull + public PrepareAnvilEvent getEvent() { + return event; + } + + public CAPreAnvilBypassEvent(@NotNull PrepareAnvilEvent event) { + this.event = event; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResult2Event.java b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResult2Event.java new file mode 100644 index 0000000..30c5380 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResult2Event.java @@ -0,0 +1,196 @@ +package xyz.alexcrea.cuanvil.api.event.listener; + +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.anvil.AnvilCost; +import xyz.alexcrea.cuanvil.anvil.AnvilUseType; + +/** + * Called after custom anvil processed the click on the result on the anvil inventory. + * This event should be used to modify the result of an anvil use. + *

+ * You may also want to check {@link CAClickResultBypassEvent}, + * {@link CAPreAnvilBypassEvent} + * and {@link CAEarlyPreAnvilBypassEvent} for your use case + *

+ * A null result will cancel this event + */ +@SuppressWarnings("unused") +public class CATreatAnvilResult2Event extends Event { + + private static final HandlerList HANDLERS = new HandlerList(); + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + @NotNull + private final InventoryView view; + + private final AnvilUseType useType; + + @Nullable + private final ItemStack left; + @Nullable + private final ItemStack right; + + @Nullable + private ItemStack result; + + private final AnvilCost cost; + + @ApiStatus.Internal + public CATreatAnvilResult2Event( + @NotNull InventoryView view, + Inventory inv, + AnvilUseType useType, + @Nullable ItemStack result, + AnvilCost cost) { + this.view = view; + this.useType = useType; + + this.left = inv.getItem(0); // TODO use view here + this.right = inv.getItem(1); + this.result = result; + this.cost = cost; + } + + /** + * Get the bukkit inventory view. + *

+ * Temporarily marked as internal as it will get changed to anvil view on legacy removal + * so signature will change + * + * @return The inventory view of this event. + */ + @ApiStatus.Internal + public @NotNull InventoryView getView() { + return view; + } + + + /** + * Get the type of use source of the result. + * + * @return The craft use type. + */ + public AnvilUseType getUseType() { + return useType; + } + + /** + * Get the left item of the anvil use + * + * @return the left item + */ + public @Nullable ItemStack getLeftItem() { + return left; + } + + /** + * Get the right item of the anvil use + * + * @return the right item + */ + public @Nullable ItemStack getRightItem() { + return right; + } + + /** + * Get the current result + *

+ * note that it will not be null unless another listener previously set it to null. + * + * @return The current result. + */ + public @Nullable ItemStack getResult() { + return result; + } + + /** + * Set the current result + *

+ * note that a null result will cancel this anvil use. + * + * @param result The new result + */ + public void setResult(@Nullable ItemStack result) { + this.result = result; + } + + /** + * Get the level cost displayed on the anvil. + *

Important note:

+ * the final price are re calculated on click for the following use case: + *
    + *
  • Custom craft
  • + *
  • Unit repair
  • + *
  • Lore edit
  • + *
+ * This value will be used as final price for: + *
  • Item merge
  • + *
  • Item rename
  • + * + * + * @return The current cost. + * @deprecated use #{@link #getCost()} instead + */ + @Deprecated(forRemoval = true, since = "1.17.0") + public int getLevelCost() { + return cost.asXpCost(); + } + + /** + * Set the level cost displayed on the anvil. + *

    Important note:

    + * the final price are re calculated on click for the following use case: + *
      + *
    • Custom craft
    • + *
    • Unit repair
    • + *
    • Lore edit
    • + *
    + * This value will be used as final price for: + *
  • Item merge
  • + *
  • Item rename
  • + * + * + * @param levelCost The new cost. + * @deprecated use #{@link #getCost()} and set value on this instead + */ + @Deprecated(forRemoval = true, since = "1.17.0") + public void setLevelCost(int levelCost) { + cost.setGeneric(levelCost - cost.getGeneric() - cost.asXpCost()); + } + + /** + * Allow access to the current cost of the event + * Note that modifying this object will change the event resulting cost + * + *

    Important note:

    + * the final price are re calculated on click for the following use case: + *
      + *
    • Custom craft
    • + *
    • Unit repair
    • + *
    • Lore edit
    • + *
    + * This value will be used as final price for: + *
  • Item merge
  • + *
  • Item rename
  • + * + * @return the current anvil cost + */ + public AnvilCost getCost() { + return cost; + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResultEvent.java b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResultEvent.java new file mode 100644 index 0000000..80965b5 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResultEvent.java @@ -0,0 +1,162 @@ +package xyz.alexcrea.cuanvil.api.event.listener; + +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.bukkit.event.inventory.PrepareAnvilEvent; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.anvil.AnvilCost; +import xyz.alexcrea.cuanvil.anvil.AnvilUseType; + +/** + * Called after custom anvil processed the click on the result on the anvil inventory. + * This event should be used to modify the result of an anvil use. + *

    + * You may also want to check {@link CAClickResultBypassEvent}, + * {@link CAPreAnvilBypassEvent} + * and {@link CAEarlyPreAnvilBypassEvent} for your use case + *

    + * A null result will cancel this pre anvil event + * + * @deprecated Prepare anvil Event cannot be provided as it can be called on result and therefore not have prepared anvil event + * use {@link CATreatAnvilResult2Event} instead + */ +@SuppressWarnings("unused") +@Deprecated(forRemoval = true, since = "1.17.0") +public class CATreatAnvilResultEvent extends Event { + + private static final HandlerList HANDLERS = new HandlerList(); + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + @NotNull + private final PrepareAnvilEvent event; + + private final AnvilUseType useType; + + @Nullable + private ItemStack result; + + private final AnvilCost cost; + + public CATreatAnvilResultEvent(@NotNull PrepareAnvilEvent event, AnvilUseType useType, @Nullable ItemStack result, AnvilCost cost) { + this.event = event; + this.useType = useType; + this.result = result; + this.cost = cost; + } + + /** + * Get the bukkit inventory click event causing to this event. + * + * @return The click event causing to this event. + */ + public @NotNull PrepareAnvilEvent getEvent() { + return event; + } + + /** + * Get the type of use source of the result. + * + * @return The craft use type. + */ + public AnvilUseType getUseType() { + return useType; + } + + /** + * Get the current result + *

    + * note that it will not be null unless another listener previously set it to null. + * + * @return The current result. + */ + public @Nullable ItemStack getResult() { + return result; + } + + /** + * Set the current result + *

    + * note that a null result will cancel this anvil use. + * + * @param result The new result + */ + public void setResult(@Nullable ItemStack result) { + this.result = result; + } + + /** + * Get the level cost displayed on the anvil. + *

    Important note:

    + * the final price are re calculated on click for the following use case: + *
      + *
    • Custom craft
    • + *
    • Unit repair
    • + *
    • Lore edit
    • + *
    + * This value will be used as final price for: + *
  • Item merge
  • + *
  • Item rename
  • + * + * + * @return The current cost. + * @deprecated use #{@link #getCost()} instead + */ + @Deprecated(forRemoval = true, since = "1.17.0") + public int getLevelCost() { + return cost.asXpCost(); + } + + /** + * Set the level cost displayed on the anvil. + *

    Important note:

    + * the final price are re calculated on click for the following use case: + *
      + *
    • Custom craft
    • + *
    • Unit repair
    • + *
    • Lore edit
    • + *
    + * This value will be used as final price for: + *
  • Item merge
  • + *
  • Item rename
  • + * + * + * @param levelCost The new cost. + * @deprecated use #{@link #getCost()} and set value on this instead + */ + @Deprecated(forRemoval = true, since = "1.17.0") + public void setLevelCost(int levelCost) { + cost.setGeneric(levelCost - cost.getGeneric() - cost.asXpCost()); + } + + /** + * Allow access to the current cost of the event + * Note that modifying this object will change the event resulting cost + * + *

    Important note:

    + * the final price are re calculated on click for the following use case: + *
      + *
    • Custom craft
    • + *
    • Unit repair
    • + *
    • Lore edit
    • + *
    + * This value will be used as final price for: + *
  • Item merge
  • + *
  • Item rename
  • + * + * @return the current anvil cost + */ + public AnvilCost getCost() { + return cost; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/config/ConfigHolder.java b/src/main/java/xyz/alexcrea/cuanvil/config/ConfigHolder.java new file mode 100644 index 0000000..f6a7e80 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/config/ConfigHolder.java @@ -0,0 +1,416 @@ +package xyz.alexcrea.cuanvil.config; + +import com.google.common.io.Files; +import io.delilaheve.CustomAnvil; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.group.EnchantConflictManager; +import xyz.alexcrea.cuanvil.group.ItemGroupManager; +import xyz.alexcrea.cuanvil.recipe.CustomAnvilRecipeManager; +import xyz.alexcrea.cuanvil.util.MetricsUtil; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; + +@SuppressWarnings("unused") +public abstract class ConfigHolder { + + // Available configuration: + public static DefaultConfigHolder DEFAULT_CONFIG; + public static ItemGroupConfigHolder ITEM_GROUP_HOLDER; + public static ConflictConfigHolder CONFLICT_HOLDER; + public static UnitRepairHolder UNIT_REPAIR_HOLDER; + public static CustomAnvilCraftHolder CUSTOM_RECIPE_HOLDER; + + /** + * Load default configuration. + * @return True if successful. + */ + public static boolean loadDefaultConfig() { + DEFAULT_CONFIG = new DefaultConfigHolder(); + + return DEFAULT_CONFIG.reloadFromDisk(true); + } + + /** + * Load non default configuration. + * @return True if successful. + */ + public static boolean loadNonDefaultConfig() { + ITEM_GROUP_HOLDER = new ItemGroupConfigHolder(); + CONFLICT_HOLDER = new ConflictConfigHolder(); + UNIT_REPAIR_HOLDER = new UnitRepairHolder(); + CUSTOM_RECIPE_HOLDER = new CustomAnvilCraftHolder(); + + return removeNonDefaultFromDisk(true); + } + + public static boolean reloadAllFromDisk(boolean hardfail) { + boolean sucess = DEFAULT_CONFIG.reloadFromDisk(hardfail); + if (!sucess) return false; + + return removeNonDefaultFromDisk(hardfail); + } + + private static boolean removeNonDefaultFromDisk(boolean hardfail){ + boolean sucess = ITEM_GROUP_HOLDER.reloadFromDisk(hardfail); + if (!sucess) return false; + sucess = CONFLICT_HOLDER.reloadFromDisk(hardfail); + if (!sucess) return false; + sucess = UNIT_REPAIR_HOLDER.reloadFromDisk(hardfail); + if (!sucess) return false; + sucess = CUSTOM_RECIPE_HOLDER.reloadFromDisk(hardfail); + + return sucess; + } + + + // usefull part of the file + private static final File BACKUP_FOLDER = new File(CustomAnvil.instance.getDataFolder(), "backup"); + + protected FileConfiguration configuration; + + protected ConfigHolder() { + + } + + public abstract boolean reloadFromDisk(boolean hardFail); + + public abstract void reload(); + + public FileConfiguration getConfig() { + return configuration; + } + + // Config name and files + protected abstract String getConfigFileName(); + + protected String getConfigFileExtension() { + return ".yml"; + } + + protected File getConfigFile() { + return new File(CustomAnvil.instance.getDataFolder(), getConfigFileName() + getConfigFileExtension()); + } + + protected File getFirstBackup() { + return new File(BACKUP_FOLDER, getConfigFileName() + "-first" + getConfigFileExtension()); + } + + protected File getLastBackup() { + return new File(BACKUP_FOLDER, getConfigFileName() + "-latest" + getConfigFileExtension()); + } + + // Save logic + public boolean saveToDisk(boolean doBackup) { + CustomAnvil.Companion.log("Saving "+getConfigFileName()); + if (doBackup) { + if (!saveBackup()) { + CustomAnvil.instance.getLogger().severe("Could not save backup. see above."); + return false; + } + } + File base = getConfigFile(); + // if file exist and can't be deleted the file, then we gave up. + if (base.exists() && !base.delete()) { + CustomAnvil.instance.getLogger().severe("Could not save config: can't delete existing file."); + return false; + } + FileConfiguration config = getConfig(); + try { + config.save(base); + } catch (IOException e) { + e.printStackTrace(); + CustomAnvil.instance.getLogger().severe("Could not save config..."); + return false; + } + + CustomAnvil.Companion.log(getConfigFileName()+" saved successfully"); + return true; + } + + protected boolean saveBackup() { + File base = getConfigFile(); + if (!base.exists()) return true; // We did back up everything we had to (nothing in this case) + boolean sufficientSuccess = false; + + BACKUP_FOLDER.mkdirs(); + // save first backup if do not exist + File firstBackup = getFirstBackup(); + if (!firstBackup.exists()) { + try { + Files.copy(base, firstBackup); + sufficientSuccess = true; + } catch (IOException e) { + CustomAnvil.instance.getLogger().log(Level.WARNING, "Could not copy backup saving config " + base.getName(), e); + MetricsUtil.INSTANCE.trackError(e); + } + } + // save last backup + File lastBackup = getLastBackup(); + // if file exist and can't be deleted the file, then we gave up. + if (lastBackup.exists() && !lastBackup.delete()) { + return sufficientSuccess; + } + + try { + Files.move(base, lastBackup); + sufficientSuccess = true; + } catch (IOException e) { + e.printStackTrace(); + } + + return sufficientSuccess; + } + + public static class DefaultConfigHolder extends ConfigHolder { + + @Override + protected String getConfigFileName() { + return "config"; + } + + @Override + public boolean reloadFromDisk(boolean hardFail) { + CustomAnvil.instance.saveDefaultConfig(); + CustomAnvil.instance.reloadConfig(); + this.configuration = CustomAnvil.instance.getConfig(); + return true; + } + + @Override + public void reload() { + }// Nothing to do + + } + + // Abstract class for non default config + public abstract static class ResourceConfigHolder extends ConfigHolder { + + String resourceName; + + private ResourceConfigHolder(String resourceName) { + this.resourceName = resourceName; + } + + @Override + protected String getConfigFileName() { + return resourceName; + } + + @Override + public boolean reloadFromDisk(boolean hardFail) { + YamlConfiguration configuration = CustomAnvil.instance.reloadResource( + getConfigFileName() + getConfigFileExtension(), hardFail); + if (configuration == null) return false; + + this.configuration = configuration; + reload(); + + return true; + } + + } + + public abstract static class DeletableResource extends ResourceConfigHolder{ + + private static final String DELETED_FOLDER_PATH = "deleted"; + + private final @NotNull File parent; + private final @NotNull File deletedConfigFile; + + private @Nullable YamlConfiguration deletedListConfig; + private DeletableResource(String resourceName) { + super(resourceName); + this.parent = new File(CustomAnvil.instance.getDataFolder(), DELETED_FOLDER_PATH); + this.deletedConfigFile = new File(this.parent, "deleted_" + resourceName + getConfigFileExtension()); + } + + @Override + public boolean reloadFromDisk(boolean hardFail) { + if(!super.reloadFromDisk(hardFail)) return false; + loadDeletedListFile(hardFail); + + return true; + } + + private void loadDeletedListFile(boolean hardFail){ + this.deletedListConfig = CustomAnvil.instance.reloadResource(this.deletedConfigFile, hardFail); + + } + + /** + * Test if the provided element was deleted. + * @param objectPath The object path to delete. + * @return True if successful. + */ + public boolean isDeleted(String objectPath){ + if(this.deletedListConfig == null) return false; + + return this.deletedListConfig.getBoolean(objectPath, false); + } + + /** + * Delete a certain object by its path. do not save the config. + * @param objectPath The object path to delete. + * @return True if successful. + */ + public boolean delete(String objectPath){ + return delete(objectPath, false, false); + } + + /** + * Delete a certain object by its path. + * @param objectPath The object path to delete. + * @param doSave If we should save the config after deleting. + * @param doBackup If we should create a backup. + * @return True if successful. + */ + public boolean delete(String objectPath, boolean doSave, boolean doBackup){ + // Create deleted list if it does not yet exist + if(this.deletedListConfig == null){ + this.parent.mkdirs(); + try { + this.deletedConfigFile.createNewFile(); + } catch (IOException e) { + CustomAnvil.instance.getLogger().log(Level.WARNING, "Could not create " + this.deletedConfigFile.getPath(), e); + MetricsUtil.INSTANCE.trackError(e); + } + loadDeletedListFile(false); + + // Something was wrong somehow + if(this.deletedListConfig == null) return false; + } + + // Add to the deleted config + this.deletedListConfig.set(objectPath, true); + this.getConfig().set(objectPath, null); + + // Save the deleted config (may not be the most efficient, but I will handle it later) + if(doSave){ + return saveToDisk(doBackup); + } + + return true; + } + + @Override + public boolean saveToDisk(boolean doBackup) { + boolean deletedSaveSuccess = saveDeletedList(); + + return super.saveToDisk(doBackup) && deletedSaveSuccess; + } + + /** + * Save list of deleted elements. + * @return true if successful. + */ + public boolean saveDeletedList() { + if(this.deletedListConfig == null) return true; + + try { + this.deletedListConfig.save(this.deletedConfigFile); + } catch (IOException e) { + CustomAnvil.instance.getLogger().log(Level.WARNING, "Could not save " + this.deletedConfigFile.getPath(), e); + MetricsUtil.INSTANCE.trackError(e); + return false; + } + + return true; + } + + + } + + + // Class for itemGroupsManager config + public static class ItemGroupConfigHolder extends DeletableResource { + private static final String FILE_NAME = "item_groups"; + + ItemGroupManager itemGroupsManager; + + private ItemGroupConfigHolder() { + super(FILE_NAME); + } + + public ItemGroupManager getItemGroupsManager() { + return itemGroupsManager; + } + + @Override + public void reload() { + // not the most efficient way for in game reload TODO optimise + this.itemGroupsManager = new ItemGroupManager(); + this.itemGroupsManager.prepareGroups(this.configuration); + + if (CONFLICT_HOLDER.getConfig() != null) { + CONFLICT_HOLDER.reload(); + } + } + + } + + // Class for enchant conflict config + public static class ConflictConfigHolder extends DeletableResource { + private static final String FILE_NAME = "enchant_conflict"; + + EnchantConflictManager conflictManager; + + private ConflictConfigHolder() { + super(FILE_NAME); + } + + public EnchantConflictManager getConflictManager() { + return conflictManager; + } + + // We assume this is called after item group manager reload;, + @Override + public void reload() { + // not the most efficient way for in game reload TODO optimise + this.conflictManager = new EnchantConflictManager(); + this.conflictManager.prepareConflicts(this.configuration, ITEM_GROUP_HOLDER.getItemGroupsManager()); + } + + } + + // Class for unit repair config + public static class UnitRepairHolder extends DeletableResource { + private static final String ITEM_GROUP_FILE_NAME = "unit_repair_item"; + + private UnitRepairHolder() { + super(ITEM_GROUP_FILE_NAME); + } + + @Override + public void reload() { + } // Do nothing + + } + + + // Class for custom anvil craft + public static class CustomAnvilCraftHolder extends DeletableResource { + private static final String CUSTOM_RECIPE_FILE_NAME = "custom_recipes"; + CustomAnvilRecipeManager recipeManager; + + private CustomAnvilCraftHolder() { + super(CUSTOM_RECIPE_FILE_NAME); + } + + public CustomAnvilRecipeManager getRecipeManager() { + return recipeManager; + } + + @Override + public void reload() { + this.recipeManager = new CustomAnvilRecipeManager(); + this.recipeManager.prepareRecipes(this.configuration); + } + } + + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/config/WorkPenaltyType.java b/src/main/java/xyz/alexcrea/cuanvil/config/WorkPenaltyType.java new file mode 100644 index 0000000..d374999 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/config/WorkPenaltyType.java @@ -0,0 +1,66 @@ +package xyz.alexcrea.cuanvil.config; + +import com.google.common.collect.ImmutableMap; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.anvil.AnvilUseType; + +import java.util.EnumMap; + +public class WorkPenaltyType { + + public record WorkPenaltyPart( + boolean penaltyIncrease, + boolean penaltyAdditive, + boolean exclusivePenaltyIncrease, + boolean exclusivePenaltyAdditive + ) { + + @Override + public boolean equals(Object obj) { + if(!(obj instanceof WorkPenaltyPart other)) return false; + + return other.penaltyIncrease == this.penaltyIncrease && + other.penaltyAdditive == this.penaltyAdditive && + other.exclusivePenaltyIncrease == this.exclusivePenaltyIncrease && + other.exclusivePenaltyAdditive == this.exclusivePenaltyAdditive; + } + + public WorkPenaltyPart(boolean penaltyIncrease, boolean penaltyAdditive) { + this(penaltyIncrease, penaltyAdditive, false, false); + } + } + + private final EnumMap partMap; + + public WorkPenaltyType(@Nullable EnumMap partMap) { + this.partMap = new EnumMap<>(partMap != null ? partMap : new EnumMap<>(AnvilUseType.class)); + } + + public ImmutableMap getPartMap() { + return ImmutableMap.copyOf(partMap); + } + + public WorkPenaltyPart getPenaltyInfo(AnvilUseType type) { + return partMap.getOrDefault(type, type.getDefaultPenalty()); + } + + public boolean isPenaltyIncreasing(AnvilUseType type) { + return partMap.getOrDefault(type, type.getDefaultPenalty()).penaltyIncrease; + } + + public boolean isPenaltyAdditive(AnvilUseType type) { + return partMap.getOrDefault(type, type.getDefaultPenalty()).penaltyAdditive; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof WorkPenaltyType that)) return false; + + for (AnvilUseType type : AnvilUseType.getEntries()) { + if(!getPenaltyInfo(type).equals(that.getPenaltyInfo(type))) return false; + } + return true; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/AdditionalTestEnchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/AdditionalTestEnchantment.java new file mode 100644 index 0000000..821838f --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/AdditionalTestEnchantment.java @@ -0,0 +1,34 @@ +package xyz.alexcrea.cuanvil.enchant; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +public interface AdditionalTestEnchantment { + + /** + * Test if the provided enchantments can be compatible with this enchantment. only non-Custom Anvil conflict. + * @param enchantments Immutable map of validated enchantments for the item. + * @param itemType Material namespaced key of the tested item. + * @return If there is a conflict with the enchantments. + */ + boolean isEnchantConflict( + @NotNull Map enchantments, + @NotNull NamespacedKey itemType); + + /** + * Test if the provided item can be compatible with this enchantment. only non-Custom Anvil conflict. + * @param enchantments Immutable map of validated enchantments for the item. + * @param itemType Material namespaced key of the tested item. + * @param item Provide a new instance of the used item stack with the partial enchantment applied. + * @return If there is a conflict with the enchantment and the item. + */ + boolean isItemConflict( + @NotNull Map enchantments, + @NotNull NamespacedKey itemType, + @NotNull ItemStack item); + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantment.java new file mode 100644 index 0000000..ea657ac --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantment.java @@ -0,0 +1,249 @@ +package xyz.alexcrea.cuanvil.enchant; + +import org.bukkit.NamespacedKey; +import org.bukkit.entity.HumanEntity; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.enchant.bulk.BulkCleanEnchantOperation; +import xyz.alexcrea.cuanvil.enchant.bulk.BulkGetEnchantOperation; +import xyz.alexcrea.cuanvil.group.EnchantConflictGroup; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represent an enchantment compatible with Custom Anvil. + * One issue with custom anvil is: it does not handle well duplicate key name (ignoring namespace) + * as the plugin was initially coded with vanilla enchantment in head + */ +@SuppressWarnings("unused") +public interface CAEnchantment { + + + /** + * Get the default rarity of this enchant. + * @return The default rarity of this enchant. + */ + @NotNull + EnchantmentRarity defaultRarity(); + + /** + * Get the enchantment key. + * @return The enchantment key. + */ + @NotNull + NamespacedKey getKey(); + + /** + * Get the enchantment name. + * @return The enchantment name. + */ + @NotNull + String getName(); + + /** + * Get the default maximum level of this enchantment. + * @return The default maximum level of this enchantment. + */ + int defaultMaxLevel(); + + /** + * Check if the enchantment have specialised get bulk operation. + * @return If the enchantment is optimised for get bulk operation. + */ + boolean isGetOptimised(); + + /** + * Check if the enchantment have specialised clean bulk operation. + * @return If the enchantment is optimised for clean bulk operation. + */ + boolean isCleanOptimised(); + + /** + * Check if the player is allowed to use this enchantment. + * @param player The player to test. + * @return If the player is allowed to use this enchantment. + */ + boolean isAllowed(@NotNull HumanEntity player); + + /** + * Add a conflict to this enchantment conflict list. + * @param conflict The conflict to add. + */ + void addConflict(@NotNull EnchantConflictGroup conflict); + + /** + * Remove a conflict from the conflict list of this enchantment. + * @param conflict The conflict to remove from this enchantment. + */ + void removeConflict(@NotNull EnchantConflictGroup conflict); + + /** + * Clear Custom Anvil conflicts for this enchantment. + */ + void clearConflict(); + + /** + * Get a collection of Custom Anvil conflict containing this enchantment. + * @return A collection of Custom Anvil conflict containing this enchantment. + */ + @NotNull Collection getConflicts(); + + /** + * Get current level of the enchantment. + * @param item Item to search the level for. Should not get changed. + * @return Current leve of this enchantment on item. or 0 if absent. + */ + default int getLevel(@NotNull ItemStack item){ + ItemMeta meta = item.getItemMeta(); + if(meta == null) return 0; + + return getLevel(item, meta); + } + + /** + * Get current level of the enchantment. + * @param item Item to search the level for. Should not get changed. + * @param meta Meta of the provided item. Should not get changed. + * @return Current leve of this enchantment on item. or 0 if absent. + */ + int getLevel(@NotNull ItemStack item, @NotNull ItemMeta meta); + + /** + * Check if this enchantment is present on the provided level. + * @param item The item to set the enchantment level. + * @return If the enchantment have been found. + */ + boolean isEnchantmentPresent(@NotNull ItemStack item); + + /** + * Check if this enchantment is present on the provided level. + * @param item The item to set the enchantment level. + * @param meta Meta of the provided item. It will not be changed and not be set on the item. + * @return If the enchantment have been found. + */ + boolean isEnchantmentPresent(@NotNull ItemStack item, @NotNull ItemMeta meta); + + /** + * Force add an enchantment at the provided level. + * @param item The item to set the enchantment level. + * @param level The level to set the enchantment to. + */ + void addEnchantmentUnsafe(@NotNull ItemStack item, int level); + + /** + * Remove this enchantment from the provided ItemStack. + * @param item The item to remove the enchantment. + */ + void removeFrom(@NotNull ItemStack item); + + // Static functions + /** + * Clear every enchantment from this item. + * @param item Item to be cleared from enchantments. + */ + static void clearEnchants(@NotNull ItemStack item){ + // Optimised enchantment clean using item stack + for (BulkCleanEnchantOperation cleanOperator : CAEnchantmentRegistry.getInstance().getOptimisedCleanOperators()) { + cleanOperator.bulkClear(item); + } + + ItemMeta meta = item.getItemMeta(); + if(meta == null) return; + + // Optimised enchantment clean using item meta + for (BulkCleanEnchantOperation cleanOperator : CAEnchantmentRegistry.getInstance().getOptimisedCleanOperators()) { + cleanOperator.bulkClear(item, meta); + } + + item.setItemMeta(meta); + + // Clean unoptimised enchants + for (CAEnchantment enchant : CAEnchantmentRegistry.getInstance().unoptimisedCleanValues()) { + if(enchant.isEnchantmentPresent(item)){ + enchant.removeFrom(item); + } + } + + } + + /** + * Get enchantments of an item. + * @param item Item to get enchantment from. + * @return A map of the set enchantments and there's respective levels. + */ + static Map getEnchants(@NotNull ItemStack item){ + Map enchantments = new HashMap<>(); + CAEnchantmentRegistry registry = CAEnchantmentRegistry.getInstance(); + + ItemMeta meta = item.getItemMeta(); + if(meta == null) return enchantments; + + // Optimised enchantment get + for (BulkGetEnchantOperation getOperator : CAEnchantmentRegistry.getInstance().getOptimisedGetOperators()) { + getOperator.bulkGet(enchantments, item, meta); + } + + // Unoptimised enchantment get + findEnchantsFromSelectedList(item, meta, enchantments, registry.unoptimisedGetValues()); + + return enchantments; + } + + + /** + * Find enchantments of an item. only test the enchantment from the list. + * @param item Item to get enchantment from. + * @param meta Meta of the provided item. + * @param enchantments Map of enchantment to complete. + * @param enchantmentToTest Enchantment to test + */ + static void findEnchantsFromSelectedList( + @NotNull ItemStack item, + @NotNull ItemMeta meta, + @NotNull Map enchantments, + @NotNull Collection enchantmentToTest){ + for (CAEnchantment enchantment : enchantmentToTest) { + if(enchantment.isEnchantmentPresent(item, meta)){ + enchantments.put(enchantment, enchantment.getLevel(item, meta)); + } + } + + } + + /** + * Gets an array of all the registered enchantments. + * + * @param key The enchantment key + * @return Array of enchantment. + */ + static @Nullable CAEnchantment getByKey(@NotNull NamespacedKey key){ + return CAEnchantmentRegistry.getInstance().getByKey(key); + } + + /** + * Gets the enchantment by the provided name. + * @param name Name to fetch. + * @return Registered enchantment. null if absent. + * + * @deprecated use {@link #getListByName(String)} + */ + @Deprecated(since = "1.6.3") + static @Nullable CAEnchantment getByName(@NotNull String name){ + return CAEnchantmentRegistry.getInstance().getByName(name); + } + + /** + * Gets list of enchantment using the provided name. + * @param name Name to fetch. + * @return List of registered enchantment. + */ + static List getListByName(@NotNull String name){ + return CAEnchantmentRegistry.getInstance().getListByName(name); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentBase.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentBase.java new file mode 100644 index 0000000..05718d5 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentBase.java @@ -0,0 +1,115 @@ +package xyz.alexcrea.cuanvil.enchant; + +import org.bukkit.NamespacedKey; +import org.bukkit.entity.HumanEntity; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.group.EnchantConflictGroup; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Default implementation of an enchantment compatible with Custom Anvil. + * One issue with custom anvil is: it does not handle well duplicate key name (ignoring namespace) + * as the plugin was initially coded with vanilla enchantment in head + */ +public abstract class CAEnchantmentBase implements CAEnchantment { + + @NotNull + private final NamespacedKey key; + @NotNull + private final String name; + @NotNull + private final EnchantmentRarity defaultRarity; + private final int defaultMaxLevel; + + private final List conflicts; + + /** + * Constructor of Wrapped Enchantment. + * @param key The enchantment's key. + * @param defaultRarity Default rarity the enchantment should be. + * @param defaultMaxLevel Default max level the enchantment can be applied with. + */ + protected CAEnchantmentBase( + @NotNull NamespacedKey key, + @Nullable EnchantmentRarity defaultRarity, + int defaultMaxLevel){ + this.key = key; + this.name = key.getKey(); + this.defaultMaxLevel = defaultMaxLevel; + + this.defaultRarity = Objects.requireNonNullElse(defaultRarity, EnchantmentRarity.COMMON); + + this.conflicts = new ArrayList<>(); + } + + @NotNull + @Override + public final EnchantmentRarity defaultRarity(){ + return defaultRarity; + } + + @NotNull + @Override + public final NamespacedKey getKey(){ + return key; + } + + @NotNull + @Override + public final String getName(){ + return name; + } + + @Override + public final int defaultMaxLevel(){ + return defaultMaxLevel; + } + + @Override + public boolean isGetOptimised(){ + return false; + } + + @Override + public boolean isCleanOptimised(){ + return false; + } + + @Override + public boolean isAllowed(@NotNull HumanEntity player){ + return true; + } + + public boolean isEnchantmentPresent(@NotNull ItemStack item){ + ItemMeta meta = item.getItemMeta(); + if(meta == null) return false; + return isEnchantmentPresent(item, meta); + } + + @Override + public void addConflict(@NotNull EnchantConflictGroup conflict){ + this.conflicts.add(conflict); + } + + @Override + public void removeConflict(@NotNull EnchantConflictGroup conflict){ + this.conflicts.remove(conflict); + } + + @Override + public void clearConflict(){ + this.conflicts.clear(); + } + + @Override + public @NotNull List getConflicts() { + return conflicts; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentRegistry.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentRegistry.java new file mode 100644 index 0000000..854ed55 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentRegistry.java @@ -0,0 +1,250 @@ +package xyz.alexcrea.cuanvil.enchant; + +import io.delilaheve.CustomAnvil; +import org.bukkit.NamespacedKey; +import org.bukkit.enchantments.Enchantment; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.enchant.bulk.BukkitEnchantBulkOperation; +import xyz.alexcrea.cuanvil.enchant.bulk.BulkCleanEnchantOperation; +import xyz.alexcrea.cuanvil.enchant.bulk.BulkGetEnchantOperation; +import xyz.alexcrea.cuanvil.enchant.wrapped.CABukkitEnchantment; +import xyz.alexcrea.cuanvil.util.MetricsUtil; + +import java.util.*; +import java.util.logging.Level; + +public class CAEnchantmentRegistry { + + private static final CAEnchantmentRegistry instance = new CAEnchantmentRegistry(); + + public static CAEnchantmentRegistry getInstance() { + return instance; + } + + // Register enchantment functions + private final HashMap byKeyMap; + private final HashMap> byNameMap; + + private final SortedSet nameSortedEnchantments; + + private final List unoptimisedGetValues; + private final List unoptimisedCleanValues; + + private final List optimisedGetOperators; + private final List optimisedCleanOperators; + + private CAEnchantmentRegistry() { + byKeyMap = new HashMap<>(); + byNameMap = new HashMap<>(); + + nameSortedEnchantments = new TreeSet<>(Comparator.comparing(CAEnchantment::getName)); + + unoptimisedGetValues = new ArrayList<>(); + unoptimisedCleanValues = new ArrayList<>(); + + optimisedGetOperators = new ArrayList<>(); + optimisedCleanOperators = new ArrayList<>(); + } + + /** + * This should only be called on main of custom anvil. + * If called more than one time, chance of thing being broken will be high. + */ + public void registerBukkit() { + // Register enchantment + for (Enchantment enchantment : Enchantment.values()) { + register(new CABukkitEnchantment(enchantment)); + } + + // Add bukkit enchantment bulk operation + BukkitEnchantBulkOperation bukkitOperation = new BukkitEnchantBulkOperation(); + optimisedGetOperators.add(bukkitOperation); + optimisedCleanOperators.add(bukkitOperation); + } + + private static boolean hasWarnedRegistering = false; + + /** + * Can be used to register new enchantment. + *

    + * No guarantee that the enchantment will be present on the config gui if registered late. + * (By late I mean after custom anvil startup.) + * + * @param enchantment The enchantment to be registered. + * @return If the operation was successful. + */ + public boolean register(@NotNull CAEnchantment enchantment) { + if (byKeyMap.containsKey(enchantment.getKey())) { + if (Objects.equals(enchantment, byKeyMap.get(enchantment.getKey()))) { + // We are trying to register the exact same enchantment. so we just skip it. + return false; + } + + if (ConfigHolder.DEFAULT_CONFIG.getConfig().getBoolean("caution_secret_do_not_log_duplicated_registered_key", false)) { + return false; + } + + var error = new IllegalStateException("enchantment " + enchantment.getKey() + " was already registered"); + CustomAnvil.instance.getLogger().log(Level.WARNING, + "Duplicate distinct registered enchantment. This should NOT happen any time.\n" + + "If you are a custom anvil developer: Maybe custom anvil detected your enchantment as a bukkit enchantment. " + + "you should maybe remove enchantment with the same key before registering yours", + error); + MetricsUtil.INSTANCE.trackError(error); + return false; + } + + if ((!hasWarnedRegistering) && byNameMap.containsKey(enchantment.getName())) { + hasWarnedRegistering = true; + + CustomAnvil.instance.getLogger().log(Level.WARNING, + "Duplicate registered enchantment name. Please check that configuration is using namespace."); + } + + byKeyMap.put(enchantment.getKey(), enchantment); + + byNameMap.putIfAbsent(enchantment.getName(), new ArrayList<>()); + byNameMap.get(enchantment.getName()).add(enchantment); + + nameSortedEnchantments.add(enchantment); + + if (!enchantment.isGetOptimised()) { + unoptimisedGetValues.add(enchantment); + } + if (!enchantment.isCleanOptimised()) { + unoptimisedCleanValues.add(enchantment); + } + + return true; + } + + /** + * Can be used to unregister new enchantment. + * Please be cautious with this function. + * It should probably rarely be used. + *

    + * No guarantee that the enchantment will absent if the config guis if unregistered late. + * (By late I mean after custom anvil startup.) + * + * @param enchantment The enchantment to be unregistered. + * @return If the operation was successful. + */ + + public boolean unregister(@Nullable CAEnchantment enchantment) { + if (enchantment == null) return false; + byKeyMap.remove(enchantment.getKey()); + byNameMap.get(enchantment.getName()).remove(enchantment); + + nameSortedEnchantments.remove(enchantment); + + unoptimisedGetValues.remove(enchantment); + unoptimisedCleanValues.remove(enchantment); + return true; + } + + /** + * Gets the enchantment by the provided key. + * + * @param key Key to fetch. + * @return Registered enchantment. null if absent. + */ + @Nullable + public CAEnchantment getByKey(@NotNull NamespacedKey key) { + return byKeyMap.get(key); + } + + /** + * Gets the enchantment by the provided name. + * + * @param name Name to fetch. + * @return Registered enchantment. null if absent. + * @deprecated use {@link #getListByName(String)} + */ + @Deprecated(since = "1.6.3") + @Nullable + public CAEnchantment getByName(@NotNull String name) { + List enchantments = getListByName(name); + if (enchantments.isEmpty()) return null; + + return enchantments.get(0); + } + + /** + * Gets list of enchantment using the provided name. + * + * @param name Name to fetch. + * @return List of registered enchantment. + */ + @NotNull + public List getListByName(@NotNull String name) { + return byNameMap.getOrDefault(name, Collections.emptyList()); + } + + /** + * Gets an array of all the registered enchantments. + * + * @return Array of enchantments. + */ + @NotNull + public Collection values() { + return byKeyMap.values(); + } + + /** + * Gets a map of all the registered enchantments. + * + * @return Immutable map of enchantments. + */ + public Map registeredEnchantments() { + return Collections.unmodifiableMap(byKeyMap); + } + + /** + * Gets a list of all the unoptimised get operation enchantments. + * + * @return List of unoptimised enchantments. + */ + @NotNull + public List unoptimisedGetValues() { + return unoptimisedGetValues; + } + + /** + * Gets a list of all the unoptimised clean operation enchantments. + * + * @return List of unoptimised enchantments. + */ + @NotNull + public List unoptimisedCleanValues() { + return unoptimisedCleanValues; + } + + /** + * Get "clean optimised operation" for get enchantments. + * + * @return Mutable "clean enchantments optimised operation" list. + */ + public List getOptimisedCleanOperators() { + return optimisedCleanOperators; + } + + /** + * Get "get optimised operation" for get enchantments. + * + * @return Mutable "get enchantments optimised operation" list. + */ + public List getOptimisedGetOperators() { + return optimisedGetOperators; + } + + /** + * Get custom anvil enchantment sorted by name. + * + * @return An immutable sorted set of every registered enchantment sorted by name. + */ + public SortedSet getNameSortedEnchantments() { + return Collections.unmodifiableSortedSet(nameSortedEnchantments); + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/EnchantmentProperties.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/EnchantmentProperties.java new file mode 100644 index 0000000..1137feb --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/EnchantmentProperties.java @@ -0,0 +1,61 @@ +package xyz.alexcrea.cuanvil.enchant; + +// to bind EnchantmentRarity to an enchantment... +public enum EnchantmentProperties { + + AQUA_AFFINITY(EnchantmentRarity.RARE), + BANE_OF_ARTHROPODS(EnchantmentRarity.UNCOMMON), + BINDING_CURSE(EnchantmentRarity.VERY_RARE), + BLAST_PROTECTION(EnchantmentRarity.RARE), + BREACH(EnchantmentRarity.RARE), + CHANNELING(EnchantmentRarity.VERY_RARE), + DENSITY(EnchantmentRarity.UNCOMMON), + DEPTH_STRIDER(EnchantmentRarity.RARE), + EFFICIENCY(EnchantmentRarity.COMMON), + FLAME(EnchantmentRarity.RARE), + FEATHER_FALLING(EnchantmentRarity.UNCOMMON), + FIRE_ASPECT(EnchantmentRarity.RARE), + FIRE_PROTECTION(EnchantmentRarity.UNCOMMON), + FORTUNE(EnchantmentRarity.RARE), + FROST_WALKER(EnchantmentRarity.RARE), + IMPALING(EnchantmentRarity.RARE), + INFINITY(EnchantmentRarity.VERY_RARE), + KNOCKBACK(EnchantmentRarity.UNCOMMON), + LOOTING(EnchantmentRarity.RARE), + LOYALTY(EnchantmentRarity.COMMON), + LUCK_OF_THE_SEA(EnchantmentRarity.RARE), + LURE(EnchantmentRarity.RARE), + MENDING(EnchantmentRarity.RARE), + MULTISHOT(EnchantmentRarity.RARE), + PIERCING(EnchantmentRarity.COMMON), + POWER(EnchantmentRarity.COMMON), + PROJECTILE_PROTECTION(EnchantmentRarity.UNCOMMON), + PROTECTION(EnchantmentRarity.COMMON), + PUNCH(EnchantmentRarity.RARE), + QUICK_CHARGE(EnchantmentRarity.UNCOMMON), + RESPIRATION(EnchantmentRarity.RARE), + RIPTIDE(EnchantmentRarity.RARE), + SILK_TOUCH(EnchantmentRarity.VERY_RARE), + SHARPNESS(EnchantmentRarity.COMMON), + SMITE(EnchantmentRarity.UNCOMMON), + SOUL_SPEED(EnchantmentRarity.VERY_RARE), + SWIFT_SNEAK(EnchantmentRarity.VERY_RARE), + SWEEPING(EnchantmentRarity.RARE), + SWEEPING_EDGE(EnchantmentRarity.RARE), + THORNS(EnchantmentRarity.VERY_RARE), + UNBREAKING(EnchantmentRarity.UNCOMMON), + VANISHING_CURSE(EnchantmentRarity.VERY_RARE), + WIND_BURST(EnchantmentRarity.RARE), + ; + + private final EnchantmentRarity rarity; + + EnchantmentProperties(EnchantmentRarity rarity) { + this.rarity = rarity; + } + + public EnchantmentRarity getRarity() { + return rarity; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/EnchantmentRarity.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/EnchantmentRarity.java new file mode 100644 index 0000000..3718f39 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/EnchantmentRarity.java @@ -0,0 +1,52 @@ +package xyz.alexcrea.cuanvil.enchant; + +// because spigot (1.18) do not look like to provide access to enchantment rarity I need to do it myself... +public class EnchantmentRarity { + + public static final EnchantmentRarity NO_RARITY = new EnchantmentRarity(0, 0); + public static final EnchantmentRarity COMMON = new EnchantmentRarity(1); + public static final EnchantmentRarity UNCOMMON = new EnchantmentRarity(2); + public static final EnchantmentRarity RARE = new EnchantmentRarity(4); + public static final EnchantmentRarity VERY_RARE = new EnchantmentRarity(8); + + private final int itemValue; + private final int bookValue; + + private EnchantmentRarity(int itemValue, int bookValue) { + this.itemValue = itemValue; + this.bookValue = bookValue; + } + + private EnchantmentRarity(int itemValue) { + this(itemValue, Math.max(1, itemValue / 2)); + } + + public final int getBookValue() { + return bookValue; + } + + public final int getItemValue() { + return itemValue; + } + + + public static EnchantmentRarity getRarity(int itemValue, int bookValue){ + int expectedBook = Math.max(1, itemValue / 2); + if((expectedBook == bookValue) && (itemValue != 0)) return getRarity(itemValue); + + if(itemValue == 0 && bookValue == 0) return NO_RARITY; + return new EnchantmentRarity(itemValue, bookValue); + } + + public static EnchantmentRarity getRarity(int itemValue){ + return switch (itemValue) { + case 0 -> NO_RARITY; + case 1 -> COMMON; + case 2 -> UNCOMMON; + case 4 -> RARE; + case 8 -> VERY_RARE; + default -> new EnchantmentRarity(itemValue); + }; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BukkitEnchantBulkOperation.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BukkitEnchantBulkOperation.java new file mode 100644 index 0000000..73e4185 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BukkitEnchantBulkOperation.java @@ -0,0 +1,66 @@ +package xyz.alexcrea.cuanvil.enchant.bulk; + +import io.delilaheve.CustomAnvil; +import io.delilaheve.util.ConfigOptions; +import io.delilaheve.util.ItemUtil; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.api.EnchantmentApi; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; + +import java.util.Map; + +public class BukkitEnchantBulkOperation implements BulkGetEnchantOperation, BulkCleanEnchantOperation { + + @Override + public void bulkGet(@NotNull Map enchantmentMap, @NotNull ItemStack item, @NotNull ItemMeta meta) { + boolean isBook = ItemUtil.INSTANCE.isEnchantedBook(item); + + if (isBook) { + ((EnchantmentStorageMeta) meta).getStoredEnchants().forEach((enchantment, level) -> + addEnchantment(enchantmentMap, enchantment, level) + ); + } + if(!isBook || ConfigOptions.INSTANCE.getAddBookEnchantmentAsStoredEnchantment()){ + item.getEnchantments().forEach((enchantment, level) -> + addEnchantment(enchantmentMap, enchantment, level) + ); + } + } + + public void addEnchantment(@NotNull Map enchantmentMap, @NotNull Enchantment enchantment, int level) { + CAEnchantment enchant = EnchantmentApi.getByKey(enchantment.getKey()); + if (enchant == null) { + CustomAnvil.instance.getLogger().warning("Enchantment of key " + enchantment.getKey() + + " somehow not found in CustomAnvil ?"); + return; + } + + enchantmentMap.put(enchant, level); + } + + @Override + public void bulkClear(@NotNull ItemStack item) { + if (item.getType() != Material.ENCHANTED_BOOK || ConfigOptions.INSTANCE.getAddBookEnchantmentAsStoredEnchantment()) { + + item.getEnchantments().forEach((enchantment, level) -> + item.removeEnchantment(enchantment) + ); + } + } + + @Override + public void bulkClear(@NotNull ItemStack item, @NotNull ItemMeta meta) { + if (item.getType() == Material.ENCHANTED_BOOK) { + EnchantmentStorageMeta bookMeta = (EnchantmentStorageMeta) meta; + bookMeta.getStoredEnchants().forEach((enchantment, leve) -> + bookMeta.removeStoredEnchant(enchantment) + ); + } + + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BulkCleanEnchantOperation.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BulkCleanEnchantOperation.java new file mode 100644 index 0000000..4b1a225 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BulkCleanEnchantOperation.java @@ -0,0 +1,28 @@ +package xyz.alexcrea.cuanvil.enchant.bulk; + +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; + +/** + * Bulk operation for clean enchantments operations. + */ +public interface BulkCleanEnchantOperation { + + /** + * Bulk clear part of the enchantments from this item. + * The item can be edited freely. If you need the meta it is preferred to use {@link #bulkClear(ItemStack, ItemMeta)} if possible + * @param item The item to clear enchantment from. + */ + void bulkClear(@NotNull ItemStack item); + + /** + * Bulk clear part of the enchantments from this item meta. + * Item should not be edited as meta will be applied later. + * If you need to edit the item and do not need the meta use {@link #bulkClear(ItemStack)} + * @param item The item source of the item meta. should not be edited. + * @param meta The item meta to clear enchantment from. + */ + void bulkClear(@NotNull ItemStack item, @NotNull ItemMeta meta); + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BulkGetEnchantOperation.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BulkGetEnchantOperation.java new file mode 100644 index 0000000..7c66e8a --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BulkGetEnchantOperation.java @@ -0,0 +1,23 @@ +package xyz.alexcrea.cuanvil.enchant.bulk; + +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; + +import java.util.Map; + +/** + * Bulk operation for get enchantments operations. + */ +public interface BulkGetEnchantOperation { + + /** + * Bulk get part of the stored enchantment of this item. + * @param enchantmentMap Mutable map of collected enchantment. should b + * @param item The item to get enchantment from. Should not get edited. + * @param meta The item meta to get enchantment from. Should not get edited. + */ + void bulkGet(@NotNull Map enchantmentMap, @NotNull ItemStack item, @NotNull ItemMeta meta); + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/EnchantSquaredBulkOperation.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/EnchantSquaredBulkOperation.java new file mode 100644 index 0000000..57ecf60 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/EnchantSquaredBulkOperation.java @@ -0,0 +1,37 @@ +package xyz.alexcrea.cuanvil.enchant.bulk; + +import me.athlaeos.enchantssquared.managers.CustomEnchantManager; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.dependency.DependencyManager; +import xyz.alexcrea.cuanvil.dependency.plugins.EnchantmentSquaredDependency; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; + +import java.util.Collections; +import java.util.Map; + +public class EnchantSquaredBulkOperation implements BulkGetEnchantOperation, BulkCleanEnchantOperation { + + @Override + public void bulkGet(@NotNull Map enchantmentMap, @NotNull ItemStack item, @NotNull ItemMeta meta) { + EnchantmentSquaredDependency enchantmentSquared = DependencyManager.INSTANCE.getEnchantmentSquaredCompatibility(); + if(enchantmentSquared != null){ + enchantmentSquared.getEnchantmentsSquared(item, enchantmentMap); + } + } + + + @Override + public void bulkClear(@NotNull ItemStack item) { + EnchantmentSquaredDependency enchantmentSquared = DependencyManager.INSTANCE.getEnchantmentSquaredCompatibility(); + if(enchantmentSquared != null){ + CustomEnchantManager.getInstance().setItemEnchants(item, Collections.emptyMap()); + } + } + + @Override + public void bulkClear(@NotNull ItemStack item, @NotNull ItemMeta meta) { + // item meta is not preferred for enchantment squared clear + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/SuperEnchantBulkOperation.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/SuperEnchantBulkOperation.java new file mode 100644 index 0000000..8bc729a --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/SuperEnchantBulkOperation.java @@ -0,0 +1,47 @@ +package xyz.alexcrea.cuanvil.enchant.bulk; + +import com.maddoxh.superEnchants.items.EnchantApplicator; +import com.maddoxh.superEnchants.items.EnchantReader; +import io.delilaheve.CustomAnvil; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.api.EnchantmentApi; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; + +import java.util.Map; + +public class SuperEnchantBulkOperation implements BulkGetEnchantOperation, BulkCleanEnchantOperation { + + private Plugin plugin; + public SuperEnchantBulkOperation(Plugin plugin) { + this.plugin = plugin; + } + + @Override + public void bulkGet(@NotNull Map enchantmentMap, @NotNull ItemStack item, @NotNull ItemMeta meta) { + EnchantReader.INSTANCE.readEnchants(item).forEach((ench, level) -> { + var enchantment = EnchantmentApi.getByKey(NamespacedKey.fromString(ench, plugin)); + if(enchantment == null) { + CustomAnvil.log("Enchantment " + ench + " not found in custom anvil"); + return; + } + + enchantmentMap.put(enchantment, level); + } + ); + } + + @Override + public void bulkClear(@NotNull ItemStack item) { + EnchantApplicator.INSTANCE.clearAllCustomEnchants(item); + } + + @Override + public void bulkClear(@NotNull ItemStack item, @NotNull ItemMeta meta) { + // item meta is not preferred for enchantment squared clear + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CABukkitEnchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CABukkitEnchantment.java new file mode 100644 index 0000000..0e630ea --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CABukkitEnchantment.java @@ -0,0 +1,174 @@ +package xyz.alexcrea.cuanvil.enchant.wrapped; + +import io.delilaheve.CustomAnvil; +import io.delilaheve.util.ConfigOptions; +import io.delilaheve.util.ItemUtil; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.enchantments.EnchantmentTarget; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.enchant.CAEnchantmentBase; +import xyz.alexcrea.cuanvil.enchant.EnchantmentProperties; +import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.logging.Level; + +/** + * Custom Anvil enchantment implementation for vanilla registered enchantment. + */ +public class CABukkitEnchantment extends CAEnchantmentBase { + + public final @NotNull Enchantment bukkit; + + public CABukkitEnchantment(@NotNull Enchantment bukkit, @Nullable EnchantmentRarity rarity) { + super(bukkit.getKey(), + rarity, + bukkit.getMaxLevel()); + this.bukkit = bukkit; + } + + public CABukkitEnchantment(@NotNull Enchantment bukkit) { + this(bukkit, getRarity(bukkit)); + } + + @Override + public boolean isGetOptimised() { + return true; + } + + @Override + public boolean isCleanOptimised() { + return true; + } + + @Override + public int getLevel(@NotNull ItemStack item, @NotNull ItemMeta meta) { + if (ItemUtil.INSTANCE.isEnchantedBook(item)) { + return ((EnchantmentStorageMeta) meta).getStoredEnchantLevel(this.bukkit); + } else { + return meta.getEnchantLevel(this.bukkit); + } + } + + @Override + public boolean isEnchantmentPresent(@NotNull ItemStack item, @NotNull ItemMeta meta) { + if (ItemUtil.INSTANCE.isEnchantedBook(item)) { + EnchantmentStorageMeta bookMeta = ((EnchantmentStorageMeta) meta); + + return bookMeta.getStoredEnchants().containsKey(this.bukkit) || + (ConfigOptions.INSTANCE.getAddBookEnchantmentAsStoredEnchantment() && item.containsEnchantment(this.bukkit)); + } else { + return item.containsEnchantment(this.bukkit); + } + } + + @Override + public void addEnchantmentUnsafe(@NotNull ItemStack item, int level) { + if (ItemUtil.INSTANCE.isEnchantedBook(item)) { + EnchantmentStorageMeta bookMeta = ((EnchantmentStorageMeta) item.getItemMeta()); + + assert bookMeta != null; + bookMeta.addStoredEnchant(this.bukkit, level, true); + item.setItemMeta(bookMeta); + } else { + item.addUnsafeEnchantment(this.bukkit, level); + } + + } + + @Override + public void removeFrom(@NotNull ItemStack item) { + if (ItemUtil.INSTANCE.isEnchantedBook(item)) { + EnchantmentStorageMeta bookMeta = ((EnchantmentStorageMeta) item.getItemMeta()); + + assert bookMeta != null; + bookMeta.removeStoredEnchant(this.bukkit); + bookMeta.removeEnchant(this.bukkit); + item.setItemMeta(bookMeta); + } else { + item.removeEnchantment(this.bukkit); + } + + } + + @NotNull + public static EnchantmentRarity getRarity(Enchantment enchantment) { + try { + return EnchantmentProperties.valueOf(enchantment.getKey().getKey().toUpperCase(Locale.ENGLISH)).getRarity(); + } catch (IllegalArgumentException ignored) { + return findRarity(enchantment); + } + } + + @NotNull + protected Enchantment getEnchant() { + return this.bukkit; + } + + private static Method getAnvilCostMethod; + + static { + Class clazz = Enchantment.class; + try { + getAnvilCostMethod = clazz.getDeclaredMethod("getAnvilCost"); + getAnvilCostMethod.setAccessible(true); + + CustomAnvil.Companion.log("Detected getAnvilCost method"); + } catch (NoSuchMethodException e) { + getAnvilCostMethod = null; + } + + } + + private static final Map targetToGroup = new HashMap<>(); + static { + targetToGroup.put(EnchantmentTarget.ARMOR, "armors"); + targetToGroup.put(EnchantmentTarget.ARMOR_HEAD, "helmets"); + targetToGroup.put(EnchantmentTarget.ARMOR_TORSO, "chestplate"); + targetToGroup.put(EnchantmentTarget.ARMOR_LEGS, "leggings"); + targetToGroup.put(EnchantmentTarget.ARMOR_FEET, "boots"); + targetToGroup.put(EnchantmentTarget.BOW, "bow"); + targetToGroup.put(EnchantmentTarget.BREAKABLE, "can_unbreak"); + targetToGroup.put(EnchantmentTarget.CROSSBOW, "crossbow"); + targetToGroup.put(EnchantmentTarget.FISHING_ROD, "fishing_rod"); + targetToGroup.put(EnchantmentTarget.TOOL, "tools"); + targetToGroup.put(EnchantmentTarget.TRIDENT, "trident"); + targetToGroup.put(EnchantmentTarget.VANISHABLE, "can_vanish"); + targetToGroup.put(EnchantmentTarget.WEAPON, "swords"); + targetToGroup.put(EnchantmentTarget.WEARABLE, "wearable"); + } + + private static EnchantmentRarity findRarity(Enchantment enchantment) { + if (getAnvilCostMethod == null) return EnchantmentRarity.COMMON; + + try { + int itemCost = (int) getAnvilCostMethod.invoke(enchantment); + + return EnchantmentRarity.getRarity(itemCost); + } catch (IllegalAccessException | InvocationTargetException e) { + CustomAnvil.instance.getLogger().log(Level.SEVERE, "could not find cost for enchantment " + enchantment.getKey(), e); + + return EnchantmentRarity.COMMON; + } + + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof CABukkitEnchantment other)) { + return false; + } + + return Objects.equals(this.bukkit, other.getEnchant()); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEPreV5Enchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEPreV5Enchantment.java new file mode 100644 index 0000000..783798d --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEPreV5Enchantment.java @@ -0,0 +1,61 @@ +package xyz.alexcrea.cuanvil.enchant.wrapped; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import su.nightexpress.excellentenchants.api.enchantment.CustomEnchantment; +import su.nightexpress.excellentenchants.api.enchantment.Definition; +import xyz.alexcrea.cuanvil.enchant.AdditionalTestEnchantment; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Set; + +public class CAEEPreV5Enchantment extends CABukkitEnchantment implements AdditionalTestEnchantment { + + @NotNull CustomEnchantment eeenchantment; + @NotNull Definition definition; + + public CAEEPreV5Enchantment(@NotNull CustomEnchantment enchantment) { + super(enchantment.getBukkitEnchantment(), getRarity(enchantment.getBukkitEnchantment())); + this.eeenchantment = enchantment; + try { + this.definition = (Definition) getDefinition.invoke(enchantment); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + + } + + private final static Method getDefinition; + static { + try { + getDefinition = CustomEnchantment.class.getMethod("getDefinition"); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) { + if (!definition.hasConflicts()) return false; + + Set conflicts = definition.getConflicts(); + + for (CAEnchantment caEnchantment : enchantments.keySet()) { + if (conflicts.contains(caEnchantment.getName())) return true; + } + + return false; + } + + @Override + public boolean isItemConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType, @NotNull ItemStack item) { + if (Material.ENCHANTED_BOOK.getKey().equals(itemType)) return false; + + return !definition.getSupportedItems().is(item); + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java new file mode 100644 index 0000000..2d8f945 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java @@ -0,0 +1,128 @@ +package xyz.alexcrea.cuanvil.enchant.wrapped; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import su.nightexpress.excellentenchants.api.enchantment.CustomEnchantment; +import su.nightexpress.excellentenchants.api.item.ItemSet; +import xyz.alexcrea.cuanvil.enchant.AdditionalTestEnchantment; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Set; + +public class CAEEV5Enchantment extends CABukkitEnchantment implements AdditionalTestEnchantment { + + @NotNull CustomEnchantment eeenchantment; + @NotNull Object definition; + + public CAEEV5Enchantment(@NotNull CustomEnchantment enchantment) { + super(enchantment.getBukkitEnchantment(), EnchantmentRarity.getRarity(getAnvilCost(enchantment))); + this.eeenchantment = enchantment; + this.definition = getDefinition(enchantment); + + } + + @Override + public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) { + if (!hasConflicts()) return false; + + Set conflicts = getExclusiveSet(); + + for (CAEnchantment caEnchantment : enchantments.keySet()) { + if (conflicts.contains(caEnchantment.getName())) return true; + if (conflicts.contains(caEnchantment.getKey().toString())) return true; + } + + return false; + } + + @Override + public boolean isItemConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType, @NotNull ItemStack item) { + if (Material.ENCHANTED_BOOK.getKey().equals(itemType)) return false; + + String key = itemType.getKey(); + ItemSet primary = eeenchantment.getPrimaryItems(); + if (primary.getMaterials().contains(key)) return false; + + ItemSet supported = eeenchantment.getSupportedItems(); + if (supported.getMaterials().contains(key)) return false; + + return true; + } + + + private static final Method getDefinitonMethod; + + private static final Method getAnvilCostMethod; + private static final Method hasConflictsMethod; + private static final Method getExclusiveSetMethod; + static { + var enchClazz = CustomEnchantment.class; + try { + getDefinitonMethod = enchClazz.getDeclaredMethod("getDefinition"); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + + Class definitionClazz; + try { + definitionClazz = Class.forName("su.nightexpress.excellentenchants.api.EnchantDefinition"); + } catch (ClassNotFoundException e) { + try { + definitionClazz = Class.forName("su.nightexpress.excellentenchants.api.wrapper.EnchantDefinition"); + } catch (ClassNotFoundException ex) { + throw new RuntimeException(ex); + } + } + + // Now definition methods + try { + getAnvilCostMethod = definitionClazz.getDeclaredMethod("getAnvilCost"); + hasConflictsMethod = definitionClazz.getDeclaredMethod("hasConflicts"); + getExclusiveSetMethod = definitionClazz.getDeclaredMethod("getExclusiveSet"); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + + } + + private static Object getDefinition(CustomEnchantment enchantment) { + try { + return getDefinitonMethod.invoke(enchantment); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static int getAnvilCost(CustomEnchantment enchantment) { + try { + return (int) getAnvilCostMethod.invoke(getDefinition(enchantment)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private boolean hasConflicts() { + try { + return (boolean) hasConflictsMethod.invoke(definition); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + + private Set getExclusiveSet() { + try { + return (Set) getExclusiveSetMethod.invoke(definition); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5_4Enchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5_4Enchantment.java new file mode 100644 index 0000000..7fb8627 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5_4Enchantment.java @@ -0,0 +1,29 @@ +package xyz.alexcrea.cuanvil.enchant.wrapped; + +import org.bukkit.NamespacedKey; +import org.jetbrains.annotations.NotNull; +import su.nightexpress.excellentenchants.api.enchantment.CustomEnchantment; +import xyz.alexcrea.cuanvil.dependency.plugins.ExcellentEnchant5_4EnchantSettings; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; + +import java.util.Map; + +public class CAEEV5_4Enchantment extends CAEEV5Enchantment { + + public CAEEV5_4Enchantment(@NotNull CustomEnchantment enchantment) { + super(enchantment); + } + + @Override + public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemMat) { + if(super.isEnchantConflict(enchantments, itemMat)) return true; + + var limit = ExcellentEnchant5_4EnchantSettings.anvilLimit(); + var count = enchantments.keySet().stream() + .filter(key -> key instanceof CAEEV5_4Enchantment) + .count(); + + return count > limit; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEcoEnchant.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEcoEnchant.java new file mode 100644 index 0000000..32d1346 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEcoEnchant.java @@ -0,0 +1,79 @@ +package xyz.alexcrea.cuanvil.enchant.wrapped; + +import com.willfp.ecoenchants.enchant.EcoEnchant; +import com.willfp.ecoenchants.target.EnchantmentTarget; +import com.willfp.ecoenchants.type.EnchantmentType; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.enchant.AdditionalTestEnchantment; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity; + +import java.util.HashMap; +import java.util.Map; + +public class CAEcoEnchant extends CABukkitEnchantment implements AdditionalTestEnchantment { + + private final @NotNull EcoEnchant ecoEnchant; + + public CAEcoEnchant(@NotNull EcoEnchant enchant) { + super(enchant.getEnchantment(), EnchantmentRarity.COMMON); + this.ecoEnchant = enchant; + } + + @Override + public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) { + if (enchantments.isEmpty()) return false; + + // Check if there is only self + if (enchantments.size() == 1 && this.equals(enchantments.keySet().stream().findFirst().get())) + return false; + + if (this.ecoEnchant.getConflictsWithEverything()) { + return true; + } + + HashMap typeAmountMap = new HashMap<>(); + + for (CAEnchantment other : enchantments.keySet()) { + if (other instanceof CABukkitEnchantment otherVanilla + && this.ecoEnchant.conflictsWith(otherVanilla.getEnchant())) { + return true; + } + + if (other instanceof CAEcoEnchant ecoOther) { + EnchantmentType type = ecoOther.ecoEnchant.getType(); + typeAmountMap.putIfAbsent(type, 0); + + int amount = typeAmountMap.get(type) + 1; + if (amount > type.getLimit()) { + return true; + } + + typeAmountMap.put(type, amount); + } + + } + + return false; + } + + @Override + public boolean isItemConflict(@NotNull Map enchantments, + @NotNull NamespacedKey itemType, + @NotNull ItemStack item) { + if (Material.ENCHANTED_BOOK.getKey().equals(itemType)) { + return false; + } + + for (EnchantmentTarget target : this.ecoEnchant.getTargets()) { + if (target.matches(item)) { + return false; + } + } + + return true; + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEnchantSquaredEnchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEnchantSquaredEnchantment.java new file mode 100644 index 0000000..8f1058e --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEnchantSquaredEnchantment.java @@ -0,0 +1,79 @@ +package xyz.alexcrea.cuanvil.enchant.wrapped; + +import me.athlaeos.enchantssquared.enchantments.CustomEnchant; +import me.athlaeos.enchantssquared.managers.CustomEnchantManager; +import org.bukkit.entity.HumanEntity; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.dependency.DependencyManager; +import xyz.alexcrea.cuanvil.enchant.CAEnchantmentBase; +import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity; + +import java.util.Map; +import java.util.Objects; + +public class CAEnchantSquaredEnchantment extends CAEnchantmentBase { + + public final @NotNull CustomEnchant enchant; + + public CAEnchantSquaredEnchantment(@NotNull CustomEnchant enchant) { + super(Objects.requireNonNull( + Objects.requireNonNull(DependencyManager.INSTANCE.getEnchantmentSquaredCompatibility()).getKeyFromEnchant(enchant)), + EnchantmentRarity.COMMON, + enchant.getMaxLevel()); + this.enchant = enchant; + + } + + public @NotNull CustomEnchant getEnchant() { + return enchant; + } + + @Override + public boolean isGetOptimised() { + return true; + } + + @Override + public boolean isCleanOptimised() { + return true; + } + + @Override + public boolean isAllowed(@NotNull HumanEntity human) { + return this.enchant.hasPermission(human); + } + + @Override + public int getLevel(@NotNull ItemStack item, @NotNull ItemMeta meta) { + return CustomEnchantManager.getInstance().getEnchantStrength(item, this.enchant.getType()); + } + + @Override + public boolean isEnchantmentPresent(@NotNull ItemStack item, @NotNull ItemMeta meta) { + Map enchants = CustomEnchantManager.getInstance().getItemsEnchantsFromPDC(item); + return enchants.containsKey(this.enchant); + } + + @Override + public void addEnchantmentUnsafe(@NotNull ItemStack item, int level) { + CustomEnchantManager.getInstance().addEnchant(item, this.enchant.getType(), level); + } + + @Override + public void removeFrom(@NotNull ItemStack item) { + CustomEnchantManager.getInstance().removeEnchant(item, this.enchant.getType()); + } + + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof CAEnchantSquaredEnchantment other)) { + return false; + } + + return this.enchant.equals(other.getEnchant()); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAIncompatibleAllEnchant.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAIncompatibleAllEnchant.java new file mode 100644 index 0000000..552ecd4 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAIncompatibleAllEnchant.java @@ -0,0 +1,36 @@ +package xyz.alexcrea.cuanvil.enchant.wrapped; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.enchant.*; + +import java.util.Map; + +/** + * Represent an enchantment incompatible with every other enchantments + */ +public class CAIncompatibleAllEnchant extends CABukkitEnchantment implements AdditionalTestEnchantment { + + public CAIncompatibleAllEnchant(@NotNull Enchantment enchantment, @Nullable EnchantmentRarity rarity) { + super(enchantment, rarity); + } + + public CAIncompatibleAllEnchant(@NotNull Enchantment enchantment) { + super(enchantment); + } + + + @Override + public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) { + return !enchantments.isEmpty() && !(enchantments.size() == 1 && enchantments.containsKey(this)); + } + + @Override + public boolean isItemConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType, @NotNull ItemStack item) { + return false; + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEEEnchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEEEnchantment.java new file mode 100644 index 0000000..74068d4 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEEEnchantment.java @@ -0,0 +1,44 @@ +package xyz.alexcrea.cuanvil.enchant.wrapped; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import su.nightexpress.excellentenchants.api.enchantment.EnchantmentData; +import xyz.alexcrea.cuanvil.enchant.AdditionalTestEnchantment; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity; + +import java.util.Map; +import java.util.Set; + +public class CALegacyEEEnchantment extends CABukkitEnchantment implements AdditionalTestEnchantment { + + @NotNull EnchantmentData eeenchantment; + + public CALegacyEEEnchantment(@NotNull EnchantmentData enchantment) { + super(enchantment.getEnchantment(), EnchantmentRarity.getRarity(enchantment.getAnvilCost())); + this.eeenchantment = enchantment; + + } + + @Override + public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) { + if (!eeenchantment.hasConflicts()) return false; + + Set conflicts = eeenchantment.getConflicts(); + + for (CAEnchantment caEnchantment : enchantments.keySet()) { + if (conflicts.contains(caEnchantment.getName())) return true; + } + + return false; + } + + @Override + public boolean isItemConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType, @NotNull ItemStack item) { + if (Material.ENCHANTED_BOOK.getKey().equals(itemType)) return false; + + return !eeenchantment.getSupportedItems().is(item); + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEcoEnchant.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEcoEnchant.java new file mode 100644 index 0000000..cb24def --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEcoEnchant.java @@ -0,0 +1,68 @@ +package xyz.alexcrea.cuanvil.enchant.wrapped; + +import com.willfp.ecoenchants.enchantments.EcoEnchant; +import com.willfp.ecoenchants.enchantments.meta.EnchantmentTarget; +import com.willfp.ecoenchants.enchantments.meta.EnchantmentType; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.enchant.AdditionalTestEnchantment; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity; +import xyz.alexcrea.cuanvil.util.MaterialUtil; + +import java.util.Map; + +public class CALegacyEcoEnchant extends CABukkitEnchantment implements AdditionalTestEnchantment { + + private final @NotNull EcoEnchant ecoEnchant; + + public CALegacyEcoEnchant(@NotNull EcoEnchant ecoEnchant, @NotNull Enchantment enchantment) { + super(enchantment, EnchantmentRarity.COMMON); + this.ecoEnchant = ecoEnchant; + } + + @Override + public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) { + if (enchantments.isEmpty()) return false; + + EnchantmentType type = this.ecoEnchant.getType(); + boolean isSingular = type.isSingular(); + + for (CAEnchantment other : enchantments.keySet()) { + if (other instanceof CABukkitEnchantment otherVanilla + && this.ecoEnchant.conflictsWith(otherVanilla.getEnchant())) { + return true; + } + + if (isSingular && + other != this && + (other instanceof CALegacyEcoEnchant otherEco) && + type.equals(otherEco.ecoEnchant.getType())) { + return true; + } + } + + return false; + } + + @Override + public boolean isItemConflict(@NotNull Map enchantments, + @NotNull NamespacedKey itemType, + @NotNull ItemStack item) { + if (Material.ENCHANTED_BOOK.getKey().equals(itemType)) { + return false; + } + + var mat = MaterialUtil.INSTANCE.getMatFromKey(itemType); + for (EnchantmentTarget target : this.ecoEnchant.getTargets()) { + if (target.getMaterials().contains(mat)) { + return false; + } + } + + return true; + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CASuperEnchantEnchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CASuperEnchantEnchantment.java new file mode 100644 index 0000000..6039dc8 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CASuperEnchantEnchantment.java @@ -0,0 +1,76 @@ +package xyz.alexcrea.cuanvil.enchant.wrapped; + +import com.maddoxh.superEnchants.enchants.CustomEnchant; +import com.maddoxh.superEnchants.enchants.EnchantManager; +import com.maddoxh.superEnchants.items.EnchantApplicator; +import com.maddoxh.superEnchants.items.EnchantReader; +import com.maddoxh.superEnchants.util.ConflictChecker; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.enchant.AdditionalTestEnchantment; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.enchant.CAEnchantmentBase; +import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity; + +import java.util.HashMap; +import java.util.Map; + +public class CASuperEnchantEnchantment extends CAEnchantmentBase implements AdditionalTestEnchantment { + + private @NotNull CustomEnchant enchant; + private @NotNull EnchantManager enchantManager; + + public CASuperEnchantEnchantment(@NotNull CustomEnchant enchant, @NotNull Plugin plugin, @NotNull EnchantManager enchantManager) { + super(NamespacedKey.fromString(enchant.getId(), plugin), EnchantmentRarity.COMMON, enchant.getMaxLevel()); + + this.enchant = enchant; + this.enchantManager = enchantManager; + } + + @Override + public int getLevel(@NotNull ItemStack item, @NotNull ItemMeta meta) { + return EnchantReader.INSTANCE.getEnchantLevel(item, enchant.getId()); + } + + @Override + public boolean isEnchantmentPresent(@NotNull ItemStack item, @NotNull ItemMeta meta) { + return EnchantReader.INSTANCE.hasEnchant(item, enchant.getId()); + } + + @Override + public void addEnchantmentUnsafe(@NotNull ItemStack item, int level) { + EnchantApplicator.INSTANCE.applyEnchant(item, enchant.getId(), level); + } + + @Override + public void removeFrom(@NotNull ItemStack item) { + EnchantApplicator.INSTANCE.removeEnchant(item, enchant.getId()); + } + + @Override + public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) { + var idMap = new HashMap(); + + enchantments.forEach((enchant, level) -> { + if(!(enchant instanceof CASuperEnchantEnchantment superEnch)) return; + idMap.put(superEnch.enchant.getId(), level); + }); + + return ConflictChecker.INSTANCE.hasConflict( + idMap, + enchant.getId(), + enchantManager + ) != null; + } + + @Override + public boolean isItemConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType, @NotNull ItemStack item) { + if(Material.ENCHANTED_BOOK.equals(item.getType())) return false; + + return !enchant.canApplyTo(item.getType()); + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/ValueUpdatableGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/ValueUpdatableGui.java new file mode 100644 index 0000000..16bc082 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/ValueUpdatableGui.java @@ -0,0 +1,11 @@ +package xyz.alexcrea.cuanvil.gui; + +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; + +public interface ValueUpdatableGui { + + void updateGuiValues(); + + Gui getConnectedGui(); + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/MainConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/MainConfigGui.java new file mode 100644 index 0000000..cc4fddc --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/MainConfigGui.java @@ -0,0 +1,154 @@ +package xyz.alexcrea.cuanvil.gui.config; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.ChestGui; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager; +import xyz.alexcrea.cuanvil.gui.config.global.*; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; + +import java.util.Collections; + +public class MainConfigGui extends ChestGui { + + private static final MainConfigGui INSTANCE = new MainConfigGui(); + + public static MainConfigGui getInstance() { + return INSTANCE; + } + + private MainConfigGui() { + super(3, "§8Anvil Config", CustomAnvil.instance); + } + + public void init(PacketManager packetManager) { + Pattern pattern = new Pattern( + GuiSharedConstant.EMPTY_GUI_FULL_LINE, + "012345678", + "Q00000000" + ); + PatternPane pane = new PatternPane(0, 0, 9, 3, pattern); + addPane(pane); + + GuiGlobalItems.addBackgroundItem(pane); + + // Basic config item + ItemStack basicConfigItemstack = new ItemStack(Material.COMMAND_BLOCK); + ItemMeta basicConfigMeta = basicConfigItemstack.getItemMeta(); + assert basicConfigMeta != null; + + basicConfigMeta.setDisplayName("§aBasic Config Menu"); + basicConfigMeta.setLore(Collections.singletonList("§7Click here to open basic config menu")); + basicConfigItemstack.setItemMeta(basicConfigMeta); + + GuiItem basicConfigItem = GuiGlobalItems.goToGuiItem(basicConfigItemstack, new BasicConfigGui(packetManager)); + pane.bindItem('1', basicConfigItem); + + // enchant level limit item + ItemStack enchantLimitItemstack = new ItemStack(Material.ENCHANTED_BOOK); + ItemMeta enchantLimitMeta = enchantLimitItemstack.getItemMeta(); + assert enchantLimitMeta != null; + + enchantLimitMeta.setDisplayName("§aEnchantment Level Limit"); + enchantLimitMeta.setLore(Collections.singletonList("§7Click here to open enchantment level limit menu")); + enchantLimitItemstack.setItemMeta(enchantLimitMeta); + + GuiItem enchantLimitItem = GuiGlobalItems.goToGuiItem(enchantLimitItemstack, new EnchantLimitConfigGui()); + pane.bindItem('2', enchantLimitItem); + + // enchant level limit item + ItemStack enchantMergeLimitItemstack = new ItemStack(Material.ENCHANTED_BOOK); + ItemMeta enchantMergeLimitMeta = enchantMergeLimitItemstack.getItemMeta(); + assert enchantMergeLimitMeta != null; + + enchantMergeLimitMeta.setDisplayName("§aEnchantment Merge Limit"); + enchantMergeLimitMeta.setLore(Collections.singletonList("§7Click here to open enchantment merge limit menu")); + enchantMergeLimitItemstack.setItemMeta(enchantMergeLimitMeta); + + GuiItem enchantMergeLimitItem = GuiGlobalItems.goToGuiItem(enchantMergeLimitItemstack, new EnchantMergeLimitConfigGui()); + pane.bindItem('3', enchantMergeLimitItem); + + // enchant cost item + ItemStack enchantCostItemstack = new ItemStack(Material.EXPERIENCE_BOTTLE); + ItemMeta enchantCostMeta = enchantCostItemstack.getItemMeta(); + assert enchantCostMeta != null; + + enchantCostMeta.setDisplayName("§aEnchantment Cost"); + enchantCostMeta.setLore(Collections.singletonList("§7Click here to open enchantment costs menu")); + enchantCostItemstack.setItemMeta(enchantCostMeta); + + GuiItem enchantCostItem = GuiGlobalItems.goToGuiItem(enchantCostItemstack, new EnchantCostConfigGui()); + pane.bindItem('4', enchantCostItem); + + // Enchantment Conflicts item + ItemStack enchantConflictItemstack = new ItemStack(Material.OAK_FENCE); + ItemMeta enchantConflictMeta = enchantConflictItemstack.getItemMeta(); + assert enchantConflictMeta != null; + + enchantConflictMeta.setDisplayName("§aEnchantment Conflict"); + enchantConflictMeta.setLore(Collections.singletonList("§7Click here to open enchantment conflict menu")); + enchantConflictItemstack.setItemMeta(enchantConflictMeta); + + GuiItem enchantConflictItem = GuiGlobalItems.goToGuiItem(enchantConflictItemstack, EnchantConflictGui.getInstance()); + pane.bindItem('5', enchantConflictItem); + + // Group config items + ItemStack groupItemstack = new ItemStack(Material.CHEST); + ItemMeta groupMeta = groupItemstack.getItemMeta(); + assert groupMeta != null; + + groupMeta.setDisplayName("§aItem Groups"); + groupMeta.setLore(Collections.singletonList("§7Click here to open item group menu")); + groupItemstack.setItemMeta(groupMeta); + + GuiItem groupConfigItem = GuiGlobalItems.goToGuiItem(groupItemstack, GroupConfigGui.getInstance()); + + pane.bindItem('6', groupConfigItem); + + // Unit repair item + ItemStack unirRepairItemstack = new ItemStack(Material.DIAMOND); + ItemMeta unitRepairMeta = unirRepairItemstack.getItemMeta(); + assert unitRepairMeta != null; + + unitRepairMeta.setDisplayName("§aUnit Repair"); + unitRepairMeta.setLore(Collections.singletonList("§7Click here to open anvil unit repair menu")); + unirRepairItemstack.setItemMeta(unitRepairMeta); + + GuiItem unitRepairItem = GuiGlobalItems.goToGuiItem(unirRepairItemstack, UnitRepairConfigGui.getInstance()); + pane.bindItem('7', unitRepairItem); + + // Custom recipe item + ItemStack customRecipeItemstack = new ItemStack(Material.CRAFTING_TABLE); + ItemMeta customRecipeMeta = customRecipeItemstack.getItemMeta(); + assert customRecipeMeta != null; + + customRecipeMeta.setDisplayName("§aCustom recipes"); + customRecipeMeta.setLore(Collections.singletonList("§7Click here to open anvil custom recipe menu")); + customRecipeItemstack.setItemMeta(customRecipeMeta); + + GuiItem customRecipeItem = GuiGlobalItems.goToGuiItem(customRecipeItemstack, CustomRecipeConfigGui.getInstance()); + pane.bindItem('8', customRecipeItem); + + // quit item + ItemStack quitItemstack = new ItemStack(Material.BARRIER); + ItemMeta quitMeta = quitItemstack.getItemMeta(); + assert quitMeta != null; + + quitMeta.setDisplayName("§cQuit"); + quitItemstack.setItemMeta(quitMeta); + + GuiItem quitItem = new GuiItem(quitItemstack, event -> { + event.setCancelled(true); + event.getWhoClicked().closeInventory(); + }, CustomAnvil.instance); + pane.bindItem('Q', quitItem); + + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectEnchantmentContainer.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectEnchantmentContainer.java new file mode 100644 index 0000000..1174209 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectEnchantmentContainer.java @@ -0,0 +1,15 @@ +package xyz.alexcrea.cuanvil.gui.config; + +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; + +import java.util.Set; + +public interface SelectEnchantmentContainer { + + Set getSelectedEnchantments(); + + boolean setSelectedEnchantments(Set enchantments); + + Set illegalEnchantments(); + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectGroupContainer.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectGroupContainer.java new file mode 100644 index 0000000..49f8b3b --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectGroupContainer.java @@ -0,0 +1,45 @@ +package xyz.alexcrea.cuanvil.gui.config; + +import xyz.alexcrea.cuanvil.group.AbstractMaterialGroup; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +public interface SelectGroupContainer { + + Set getSelectedGroups(); + + boolean setSelectedGroups(Set groups); + + Set illegalGroups(); + + static List getGroupLore(SelectGroupContainer container, String containerType, String groupAction){ + // Prepare group lore + ArrayList groupLore = new ArrayList<>(); + groupLore.add("§7Allow you to select a list of §3Groups §7that this " + containerType + " should " + groupAction); + Set grouos = container.getSelectedGroups(); + if (grouos.isEmpty()) { + groupLore.add("§7There is no "+groupAction+"d group for this "+containerType+"."); + } else { + groupLore.add("§7List of "+groupAction+"d groups for this "+containerType+":"); + Iterator groupIterator = grouos.iterator(); + + boolean greaterThanMax = grouos.size() > 5; + int maxindex = (greaterThanMax ? 4 : grouos.size()); + for (int i = 0; i < maxindex; i++) { + // format string like "- Melee Weapons" + String formattedName = CasedStringUtil.snakeToUpperSpacedCase(groupIterator.next().getName()); + groupLore.add("§7- §3" + formattedName); + + } + if (greaterThanMax) { + groupLore.add("§7And " + (grouos.size() - 4) + " more..."); + } + } + return groupLore; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectMaterialContainer.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectMaterialContainer.java new file mode 100644 index 0000000..3756341 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectMaterialContainer.java @@ -0,0 +1,43 @@ +package xyz.alexcrea.cuanvil.gui.config; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.*; + +public interface SelectMaterialContainer { + + Set getSelectedMaterials(); + + boolean setSelectedMaterials(Set materials); + + Set illegalMaterials(); + + static List getMaterialLore(SelectMaterialContainer container, String containerType, String action){ + // Prepare material lore + ArrayList groupLore = new ArrayList<>(); + groupLore.add("§7Allow you to select a list of §ematerials §7that this " + containerType + " should " + action); + Set materialSet = container.getSelectedMaterials(); + if (materialSet.isEmpty()) { + groupLore.add("§7There is no "+action+"d material for this "+containerType+"."); + } else { + groupLore.add("§7List of "+action+"d materials for this "+containerType+":"); + Iterator materialIterator = materialSet.iterator(); + + boolean greaterThanMax = materialSet.size() > 5; + int maxindex = (greaterThanMax ? 4 : materialSet.size()); + for (int i = 0; i < maxindex; i++) { + // format string like "- Stone Sword" + String formattedName = CasedStringUtil.snakeToUpperSpacedCase(materialIterator.next().getKey().toLowerCase()); + groupLore.add("§7- §e" + formattedName); + + } + if (greaterThanMax) { + groupLore.add("§7And " + (materialSet.size() - 4) + " more..."); + } + } + return groupLore; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/AbstractAskGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/AbstractAskGui.java new file mode 100644 index 0000000..66dd936 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/AbstractAskGui.java @@ -0,0 +1,42 @@ +package xyz.alexcrea.cuanvil.gui.config.ask; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.ChestGui; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; + +public abstract class AbstractAskGui extends ChestGui { + + protected PatternPane pane; + AbstractAskGui(int rows, @NotNull String name, + Gui backOnCancel){ + super(rows, name, CustomAnvil.instance); + + Pattern pattern = getGuiPattern(); + this.pane = new PatternPane(0, 0, pattern.getLength(), pattern.getHeight(), pattern); + addPane(this.pane); + + this.pane.bindItem('0', GuiGlobalItems.backgroundItem()); + this.pane.bindItem('B', new GuiItem(GuiSharedConstant.CANCEL_ITEM, GuiGlobalActions.openGuiAction(backOnCancel), CustomAnvil.instance)); + + } + + /** + * Used to get the gui pattern. + * Reserved character are: + *

      + *
    • B: "cancel" button.
    • + *
    • 0: default background item.
    • + *
    + * + * @return The gui's pattern. + */ + protected abstract Pattern getGuiPattern(); + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/ConfirmActionGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/ConfirmActionGui.java new file mode 100644 index 0000000..5839663 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/ConfirmActionGui.java @@ -0,0 +1,84 @@ +package xyz.alexcrea.cuanvil.gui.config.ask; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.entity.HumanEntity; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; +import xyz.alexcrea.cuanvil.util.MetricsUtil; + +import java.util.Arrays; +import java.util.function.Supplier; +import java.util.logging.Level; + +public class ConfirmActionGui extends AbstractAskGui { + + public ConfirmActionGui(@NotNull String title, String actionDescription, + Gui backOnCancel, Gui backOnConfirm, Supplier onConfirm, + boolean permanent) { + super(3, title, backOnCancel); + + // Save item + this.pane.bindItem('S', new GuiItem( + (permanent ? GuiSharedConstant.CONFIRM_PERMANENT_ITEM : GuiSharedConstant.CONFIRM_ITEM), + event -> { + event.setCancelled(true); + HumanEntity player = event.getWhoClicked(); + + if (!player.hasPermission(CustomAnvil.editConfigPermission)) { + player.closeInventory(); + player.sendMessage(GuiGlobalActions.NO_EDIT_PERM); + return; + } + + boolean success; + try { + success = onConfirm.get(); + } catch (Exception e) { + CustomAnvil.instance.getLogger().log(Level.WARNING, "Could not process confirmation supplier.", e); + MetricsUtil.INSTANCE.trackError(e); + success = false; + } + + if (!success) { + event.getWhoClicked().sendMessage("§cAction could not be completed. "); + } + backOnConfirm.show(player); + + }, CustomAnvil.instance)); + + // Info item + ItemStack infoItem = new ItemStack(Material.PAPER); + ItemMeta infoMeta = infoItem.getItemMeta(); + + infoMeta.setDisplayName("§eAre you sure ?"); + if(actionDescription != null){ + infoMeta.setLore(Arrays.asList(actionDescription.split("\n"))); + } + + infoItem.setItemMeta(infoMeta); + + pane.bindItem('I', new GuiItem(infoItem, GuiGlobalActions.stayInPlace, CustomAnvil.instance)); + } + public ConfirmActionGui(@NotNull String title, String actionDescription, + Gui backOnCancel, Gui backOnConfirm, Supplier onConfirm){ + this(title, actionDescription, backOnCancel, backOnConfirm, onConfirm, true); + } + + + @Override + protected Pattern getGuiPattern() { + return new Pattern( + GuiSharedConstant.EMPTY_GUI_FULL_LINE, + "00B0I0S00", + GuiSharedConstant.EMPTY_GUI_FULL_LINE + ); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/SelectItemTypeGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/SelectItemTypeGui.java new file mode 100644 index 0000000..66411bd --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/SelectItemTypeGui.java @@ -0,0 +1,99 @@ +package xyz.alexcrea.cuanvil.gui.config.ask; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.entity.HumanEntity; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; +import xyz.alexcrea.cuanvil.util.MaterialUtil; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; + +public class SelectItemTypeGui extends AbstractAskGui { + + private ItemStack selectedItem; + public SelectItemTypeGui(@NotNull String title, + @NotNull String actionDescription, + @NotNull Gui backOnCancel, + @NotNull BiConsumer onSave, + boolean materialOnly) { + super(3, title, backOnCancel); + this.selectedItem = null; + + // Save item + GuiItem confirmItem = new GuiItem(GuiSharedConstant.CONFIRM_ITEM, event -> { + event.setCancelled(true); + HumanEntity player = event.getWhoClicked(); + + if (!player.hasPermission(CustomAnvil.editConfigPermission)) { + player.closeInventory(); + player.sendMessage(GuiGlobalActions.NO_EDIT_PERM); + return; + } + + onSave.accept(this.selectedItem, player); + + }, CustomAnvil.instance); + this.pane.bindItem('S', GuiGlobalItems.backgroundItem()); + + // Select item + ItemStack selectItem = setDisplayMeta(new ItemStack(Material.BARRIER), actionDescription); + + AtomicReference selectGuiItem = new AtomicReference<>(); + selectGuiItem.set(new GuiItem(selectItem, event -> { + event.setCancelled(true); + + ItemStack cursor = event.getWhoClicked().getItemOnCursor(); + if(MaterialUtil.INSTANCE.isAir(cursor)) return; + + ItemStack finalItem; + if(materialOnly){ + finalItem = setDisplayMeta(new ItemStack(cursor.getType()), actionDescription); + }else{ + finalItem = cursor.clone(); + } + this.selectedItem = finalItem.clone(); + + selectGuiItem.get().setItem(finalItem); + this.pane.bindItem('S', confirmItem); + + update(); + }, CustomAnvil.instance)); + + this.pane.bindItem('V', selectGuiItem.get()); + + // Temporary leave item + GuiItem temporaryLeave = GuiGlobalItems.temporaryCloseGuiToSelectItem(Material.YELLOW_STAINED_GLASS_PANE, this); + + this.pane.bindItem('s', temporaryLeave); + + } + + private ItemStack setDisplayMeta(ItemStack item, String actionDescription){ + ItemMeta meta = item.getItemMeta(); + + meta.setDisplayName("§ePlace an item here"); + meta.setLore(Arrays.asList(actionDescription.split("\n"))); + + item.setItemMeta(meta); + return item; + } + + @Override + protected Pattern getGuiPattern() { + return new Pattern( + GuiSharedConstant.EMPTY_GUI_FULL_LINE, + "0000V000s", + "B0000000S" + ); + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/AbstractEnchantConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/AbstractEnchantConfigGui.java new file mode 100644 index 0000000..6bd7ea3 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/AbstractEnchantConfigGui.java @@ -0,0 +1,117 @@ +package xyz.alexcrea.cuanvil.gui.config.global; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import org.bukkit.event.inventory.InventoryClickEvent; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry; +import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui; +import xyz.alexcrea.cuanvil.gui.config.list.SettingGuiListConfigGui; +import xyz.alexcrea.cuanvil.gui.config.settings.SettingGui; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +/** + * Abstract Global Config gui for enchantment setting configuration. + * + * @param Type of the factory of the type of setting the gui should edit. + */ +public abstract class AbstractEnchantConfigGui extends SettingGuiListConfigGui{ + + /** + * Constructor for a gui displaying available enchantment to edit a enchantment setting. + * + * @param title Title of the gui. + */ + protected AbstractEnchantConfigGui(String title) { + super(title); + } + + @Override + public void updateGuiValues() { //TODO maybe optimise it. + reloadValues(); + } + + @Override + protected Collection getEveryDisplayableInstanceOfGeneric() { + return CAEnchantmentRegistry.getInstance().getNameSortedEnchantments(); + } + + @Override + protected Pattern getBackgroundPattern(){ + return new Pattern( + GuiSharedConstant.UPPER_FILLER_FULL_PLANE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + "B11L1R111" + ); + } + + @Override + public void updateValueForGeneric(CAEnchantment generic, boolean shouldUpdate) { + updateValueForGeneric(generic, shouldUpdate, true); + } + + public void updateValueForGeneric(CAEnchantment generic, boolean shouldUpdate, boolean prepareSorting) { + if(!prepareSorting) { + super.updateValueForGeneric(generic, shouldUpdate); + return; + } + + if(!this.factoryMap.containsKey(generic)){ + // We need to sort elements again + super.updateValueForGeneric(generic, false); + + // Clear page then refill all of them + this.firstPage.clear(); + this.pages.clear(); + this.pages.add(this.firstPage); + + for (CAEnchantment enchantment : getEveryDisplayableInstanceOfGeneric()) { + GuiItem item = this.guiItemMap.get(enchantment); + + if(item == null) { + updateValueForGeneric(enchantment, false, false); + }else { + addToPage(item); + } + + } + + if(shouldUpdate) update(); + + }else{ + super.updateValueForGeneric(generic, shouldUpdate); + + } + + } + + + // Unused methods + @Override + protected GuiItem prepareCreateNewItem() { + return null; + } + + @Override + protected List getCreateItemLore() { + return Collections.emptyList(); + } + @Override + protected Consumer getCreateClickConsumer() { + return null; + } + + @Override + protected String createItemName() { + return null; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/BasicConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/BasicConfigGui.java new file mode 100644 index 0000000..51936c7 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/BasicConfigGui.java @@ -0,0 +1,371 @@ +package xyz.alexcrea.cuanvil.gui.config.global; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.ChestGui; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import io.delilaheve.util.ConfigOptions; +import kotlin.ranges.IntRange; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.dependency.MinecraftVersionUtil; +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager; +import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui; +import xyz.alexcrea.cuanvil.gui.config.MainConfigGui; +import xyz.alexcrea.cuanvil.gui.config.settings.BoolSettingsGui; +import xyz.alexcrea.cuanvil.gui.config.settings.IntSettingsGui; +import xyz.alexcrea.cuanvil.gui.config.settings.WorkPenaltyTypeSettingGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +/** + * Global config to edit basic basic settings. + */ +public class BasicConfigGui extends ChestGui implements ValueUpdatableGui { + + private static BasicConfigGui INSTANCE = null; + + @Nullable + public static BasicConfigGui getInstance() { + return INSTANCE; + } + + private final PacketManager packetManager; + /** + * Constructor of this Global gui for basic settings. + */ + public BasicConfigGui(PacketManager packetManager) { + super(4, "§8Basic Config", CustomAnvil.instance); + if(INSTANCE == null) INSTANCE = this; + + this.packetManager = packetManager; + init(); + } + + PatternPane pane; + + /** + * Initialise Basic gui + */ + private void init() { + Pattern pattern = new Pattern( + GuiSharedConstant.EMPTY_GUI_FULL_LINE, + "LT0IWS0cp", + "CR0U0r0hP", + "B00000000" + ); + pane = new PatternPane(0, 0, 9, 4, pattern); + addPane(pane); + + GuiGlobalItems.addBackItem(pane, MainConfigGui.getInstance()); + GuiGlobalItems.addBackgroundItem(pane); + + prepareValues(); + updateGuiValues(); + } + + private BoolSettingsGui.BoolSettingFactory capAnvilCost; // L character + private GuiItem noCapRepairItem; + private IntSettingsGui.IntSettingFactory maxAnvilCost; // C character + private GuiItem noMaxCostItem; + + private BoolSettingsGui.BoolSettingFactory removeAnvilCostLimit; // R character + private BoolSettingsGui.BoolSettingFactory replaceTooExpensive; // T character + + private IntSettingsGui.IntSettingFactory itemRepairCost; // I character + private IntSettingsGui.IntSettingFactory unitRepairCost; // U character + private IntSettingsGui.IntSettingFactory itemRenameCost; // r character + private IntSettingsGui.IntSettingFactory sacrificeIllegalEnchantCost; // S character + + private BoolSettingsGui.BoolSettingFactory allowColorCode; // c character + private BoolSettingsGui.BoolSettingFactory allowHexColor; // h character + + private BoolSettingsGui.BoolSettingFactory permissionNeededForColor; // p character + private GuiItem noPermissionNeededItem; + private IntSettingsGui.IntSettingFactory useOfColorCost; // P character + private GuiItem noColorCostItem; + + /** + * Prepare basic gui displayed items factory and static items.. + */ + protected void prepareValues() { + // cap anvil cost + this.capAnvilCost = new BoolSettingsGui.BoolSettingFactory("§8Cap Anvil Cost ?", this, + ConfigHolder.DEFAULT_CONFIG, + ConfigOptions.CAP_ANVIL_COST, ConfigOptions.DEFAULT_CAP_ANVIL_COST, + "§7All anvil cost will be capped to §aMax Anvil Cost§7 if enabled.", + "§7In other words:", + "§7For any anvil cost greater than §aMax Anvil Cost§7, Cost will be set to §aMax Anvil Cost§7."); + // cap anvil cost not needed + ItemStack item = new ItemStack(Material.BARRIER); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§cCap Anvil Cost ?"); + meta.setLore(Collections.singletonList("§7This config only work if §cLimit Repair Cost§7 is disabled.")); + item.setItemMeta(meta); + this.noCapRepairItem = new GuiItem(item, GuiGlobalActions.stayInPlace, CustomAnvil.instance); + + + // repair cost item + IntRange range = ConfigOptions.MAX_ANVIL_COST_RANGE; + this.maxAnvilCost = new IntSettingsGui.IntSettingFactory("§8Max Anvil Cost", this, + ConfigOptions.MAX_ANVIL_COST, ConfigHolder.DEFAULT_CONFIG, + Arrays.asList( + "§7Max cost the Anvil can get to.", + "§7Valid values include §e0 §7to §e1000§7.", + "§7Cost will be displayed as §cToo Expensive§7:", + "§7- If Cost is above §e39", + "§7- And §eReplace Too Expensive§7 is disabled" + ), + range.getFirst(), range.getLast(), + ConfigOptions.DEFAULT_MAX_ANVIL_COST, + 1, 5, 10); + // max anvil cost not needed + item = new ItemStack(Material.BARRIER); + meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§cMax Anvil Cost"); + meta.setLore(Collections.singletonList("§7This config only work if §cLimit Repair Cost§7 is disabled.")); + item.setItemMeta(meta); + this.noMaxCostItem = new GuiItem(item, GuiGlobalActions.stayInPlace, CustomAnvil.instance); + + + // remove repair limit item + this.removeAnvilCostLimit = new BoolSettingsGui.BoolSettingFactory("§8Remove Anvil Cost Limit ?", this, + ConfigHolder.DEFAULT_CONFIG, + ConfigOptions.REMOVE_ANVIL_COST_LIMIT, ConfigOptions.DEFAULT_REMOVE_ANVIL_COST_LIMIT, + "§7Whether the anvil's cost limit should be removed entirely.", + "§7The anvil will still visually display §cToo Expensive§7 if §eReplace Too Expensive§7 is disabled.", + "§7However, the action will be completable if xp requirement is meet."); + + // replace too expensive item + this.replaceTooExpensive = new BoolSettingsGui.BoolSettingFactory("§8Replace Too Expensive ?", this, + ConfigHolder.DEFAULT_CONFIG, + ConfigOptions.REPLACE_TOO_EXPENSIVE, ConfigOptions.DEFAULT_REPLACE_TOO_EXPENSIVE, + getReplaceToExpensiveLore()); + + // ------------ + // Cost config + // ------------ + + // item repair cost + range = ConfigOptions.REPAIR_COST_RANGE; + this.itemRepairCost = new IntSettingsGui.IntSettingFactory("§8Item Repair Cost", this, + ConfigOptions.ITEM_REPAIR_COST, ConfigHolder.DEFAULT_CONFIG, + Arrays.asList( + "§7XP Level amount added to the anvil when the item", + "§7is repaired by another item of the same type." + ), + range.getFirst(), range.getLast(), + ConfigOptions.DEFAULT_ITEM_REPAIR_COST, + 1, 5, 10, 50, 100); + + // unit repair cost + this.unitRepairCost = new IntSettingsGui.IntSettingFactory("§8Unit Repair Cost", this, + ConfigOptions.UNIT_REPAIR_COST, ConfigHolder.DEFAULT_CONFIG, + Arrays.asList( + "§7XP Level amount added to the anvil when the item is repaired by an §eunit§7.", + "§7For example: a Diamond on a Diamond Sword.", + "§7What's considered unit for what can be edited on the unit repair configuration." + ), + range.getFirst(), range.getLast(), + ConfigOptions.DEFAULT_UNIT_REPAIR_COST, + 1, 5, 10, 50, 100); + + // item rename cost + range = ConfigOptions.ITEM_RENAME_COST_RANGE; + this.itemRenameCost = new IntSettingsGui.IntSettingFactory("§8Rename Cost", this, + ConfigOptions.ITEM_RENAME_COST, ConfigHolder.DEFAULT_CONFIG, + Arrays.asList( + "§7XP Level amount added to the anvil when the item is renamed." + ), + range.getFirst(), range.getLast(), + ConfigOptions.DEFAULT_ITEM_RENAME_COST, + 1, 5, 10, 50, 100); + + // sacrifice illegal enchant cost + range = ConfigOptions.SACRIFICE_ILLEGAL_COST_RANGE; + this.sacrificeIllegalEnchantCost = new IntSettingsGui.IntSettingFactory("§8Sacrifice Illegal Enchant Cost", this, + ConfigOptions.SACRIFICE_ILLEGAL_COST, ConfigHolder.DEFAULT_CONFIG, + Arrays.asList( + "§7XP Level amount added to the anvil when a sacrifice enchantment", + "§7conflict With one of the left item enchantment" + ), + range.getFirst(), range.getLast(), + ConfigOptions.DEFAULT_SACRIFICE_ILLEGAL_COST, + 1, 5, 10, 50, 100); + + // ------------- + // Color config + // ------------- + + // Allow us of color code + this.allowColorCode = new BoolSettingsGui.BoolSettingFactory("§8Allow Use Of Color Code ?", this, + ConfigHolder.DEFAULT_CONFIG, + ConfigOptions.ALLOW_COLOR_CODE, ConfigOptions.DEFAULT_ALLOW_COLOR_CODE, + "§7Whether players can use color code.", + "§7Color code a formatted like §a&a§7 and is used in the rename field of the anvil.", + "§7Player may need permission to use color code if §ePlayer need permission to use color§7 is enabled."); + + // Allow us of hexadecimal color + this.allowHexColor = new BoolSettingsGui.BoolSettingFactory("§8Allow Use Of Hexadecimal Color ?", this, + ConfigHolder.DEFAULT_CONFIG, + ConfigOptions.ALLOW_HEXADECIMAL_COLOR, ConfigOptions.DEFAULT_ALLOW_HEXADECIMAL_COLOR, + "§7Whether players can use hexadecimal color.", + "§7Color code a formatted like §2#012345 §7and is used in the rename field of the anvil.", + "§7Player may need permission to use color code if §ePermission Needed For Color§7 is enabled."); + + // Permission needed for color + this.permissionNeededForColor = new BoolSettingsGui.BoolSettingFactory("§8Need Permission To Use Color ?", this, + ConfigHolder.DEFAULT_CONFIG, + ConfigOptions.PERMISSION_NEEDED_FOR_COLOR, ConfigOptions.DEFAULT_PERMISSION_NEEDED_FOR_COLOR, + "§7Whether players should have permission to be able to use colors.", + "§7Give player §eca.color.code§7 Permission to allow use of color code.", + "§7Give player §eca.color.hex§7 Permission to allow use of hexadecimal color."); + + // Permission needed for color not necessary + item = new ItemStack(Material.BARRIER); + meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§cNeed Permission To Use Color ?"); + meta.setLore(Arrays.asList("§7This config can do something only if one of the following config is enabled:", + "§7- §aAllow Use Of Color Code", + "§7- §aAllow Use Of Hexadecimal Color")); + item.setItemMeta(meta); + this.noPermissionNeededItem = new GuiItem(item, GuiGlobalActions.stayInPlace, CustomAnvil.instance); + + // Cost of using color + range = ConfigOptions.USE_OF_COLOR_COST_RANGE; + this.useOfColorCost = new IntSettingsGui.IntSettingFactory("§8Cost Of Using Color", this, + ConfigOptions.USE_OF_COLOR_COST, ConfigHolder.DEFAULT_CONFIG, + Arrays.asList( + "§7XP level cost when using color code or hexadecimal color using the anvil.", + "§7conflict With one of the left item enchantment" + ), + range.getFirst(), range.getLast(), + ConfigOptions.DEFAULT_USE_OF_COLOR_COST, + 1, 5, 10, 50, 100); + + // Permission needed for color not necessary + item = new ItemStack(Material.BARRIER); + meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§cCost Of Using Color"); + meta.setLore(Arrays.asList("§7This config can do something only if one of the following config is enabled:", + "§7- §aAllow Use Of Color Code", + "§7- §aAllow Use Of Hexadecimal Color")); + item.setItemMeta(meta); + this.noColorCostItem = new GuiItem(item, GuiGlobalActions.stayInPlace, CustomAnvil.instance); + + } + + @NotNull + private String[] getReplaceToExpensiveLore() { + ArrayList lore = new ArrayList<>(); + lore.add("§7Whenever anvil cost is above §e39§7 should display the true price and not §cToo Expensive§7."); + lore.add("§7However, when bypassing §cToo Expensive§7, anvil price will be displayed as §aGreen§7."); + lore.add("§7Even if cost is displayed as §aGreen§7:"); + lore.add("§7If the player do not have the required xp level, the action will not be completable."); + + if(!this.packetManager.getCanSetInstantBuild()){ + lore.add(""); + lore.add("§4/!\\§cCaution§4/!\\ §cYou need ProtocoLib installed and working or a paper server."); + lore.add("§cCurrently ProtocoLib is not detected."); + } + + String[] loreAsArray = new String[lore.size()]; + return lore.toArray(loreAsArray); + } + + @Override + public void updateGuiValues() { + // limit and cap anvil cost item + GuiItem capAnvilCostItem; + GuiItem maxAnvilCostItem; + if (!this.removeAnvilCostLimit.getConfiguredValue()) { + capAnvilCostItem = this.capAnvilCost.getItem("Cap Anvil Cost"); + maxAnvilCostItem = this.maxAnvilCost.getItem(Material.EXPERIENCE_BOTTLE, "Max Anvil Cost"); + } else { + capAnvilCostItem = this.noCapRepairItem; + maxAnvilCostItem = this.noMaxCostItem; + } + + pane.bindItem('L', capAnvilCostItem); + pane.bindItem('C', maxAnvilCostItem); + + // remove repair limit item + GuiItem removeRepairLimitItem = this.removeAnvilCostLimit.getItem("Remove Anvil Cost Limit"); + pane.bindItem('R', removeRepairLimitItem); + + // replace too expensive item + GuiItem replaceToExpensiveItem = this.replaceTooExpensive.getItem(); + pane.bindItem('T', replaceToExpensiveItem); + + + // item repair cost + GuiItem itemRepairCostItem = this.itemRepairCost.getItem(Material.ANVIL); + pane.bindItem('I', itemRepairCostItem); + + // unit repair cost + GuiItem unitRepairCostItem = this.unitRepairCost.getItem(Material.DIAMOND); + pane.bindItem('U', unitRepairCostItem); + + // item rename cost + GuiItem itemRenameCostItem = this.itemRenameCost.getItem(Material.NAME_TAG); + pane.bindItem('r', itemRenameCostItem); + + // sacrifice illegal enchant cost + GuiItem illegalCostItem = this.sacrificeIllegalEnchantCost.getItem(Material.ENCHANTED_BOOK); + pane.bindItem('S', illegalCostItem); + + // work penalty type + GuiItem workPenaltyType = WorkPenaltyTypeSettingGui.getDisplayItem(this, Material.DAMAGED_ANVIL, "§aWork Penalty Type"); + pane.bindItem('W', workPenaltyType); + + // allow color code + GuiItem allowColorCodeItem = this.allowColorCode.getItem(); + pane.bindItem('c', allowColorCodeItem); + + // allow hex color + GuiItem allowHexColorItem = this.allowHexColor.getItem(); + pane.bindItem('h', allowHexColorItem); + + // True if player could place color + if(ConfigOptions.INSTANCE.getRenameColorPossible()){ + // use permission for color + GuiItem permissionNeededItem = this.permissionNeededForColor.getItem(); + pane.bindItem('p', permissionNeededItem); + + // using color cost + GuiItem useColorCostItem = this.useOfColorCost.getItem(Material.EXPERIENCE_BOTTLE, "Use color"); + pane.bindItem('P', useColorCostItem); + }else{ + pane.bindItem('p', this.noPermissionNeededItem); + pane.bindItem('P', this.noColorCostItem); + } + + + update(); + } + + @Override + public Gui getConnectedGui() { + return this; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/CustomRecipeConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/CustomRecipeConfigGui.java new file mode 100644 index 0000000..e21ad75 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/CustomRecipeConfigGui.java @@ -0,0 +1,119 @@ +package xyz.alexcrea.cuanvil.gui.config.global; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import org.bukkit.Material; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.gui.config.list.MappedGuiListConfigGui; +import xyz.alexcrea.cuanvil.gui.config.list.elements.CustomRecipeSubSettingGui; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; +import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.ArrayList; +import java.util.Collection; + +public class CustomRecipeConfigGui extends MappedGuiListConfigGui> { + + private static CustomRecipeConfigGui INSTANCE = new CustomRecipeConfigGui(); + + @Nullable + public static CustomRecipeConfigGui getCurrentInstance() { + return INSTANCE; + } + + @NotNull + public static CustomRecipeConfigGui getInstance() { + if (INSTANCE == null) INSTANCE = new CustomRecipeConfigGui(); + + return INSTANCE; + } + + private CustomRecipeConfigGui() { + super("Custom Recipe Config"); + + init(); + } + + @Override + protected ItemStack createItemForGeneric(AnvilCustomRecipe recipe) { + // Get base item to display + ItemStack craftResultItem = recipe.getResultItem(); + ItemStack displayedItem; + if (craftResultItem == null) { + displayedItem = new ItemStack(Material.BARRIER); + } else { + displayedItem = craftResultItem.clone(); + } + + // edit displayed item + ItemMeta meta = displayedItem.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§e" + CasedStringUtil.snakeToUpperSpacedCase(recipe.toString()) + " §fCustom recipe"); + meta.addItemFlags(ItemFlag.values()); + + meta.setLore(getRecipeLore(recipe)); + + displayedItem.setItemMeta(meta); + return displayedItem; + } + + private static @NotNull ArrayList getRecipeLore(AnvilCustomRecipe recipe) { + boolean shouldWork = recipe.validate(); + + ArrayList lore = new ArrayList<>(); + lore.add("§7Is valid: §" + (shouldWork ? "aYes" : "cNo")); + lore.add("§7Exact count: §" + (recipe.getExactCount() ? "aYes" : "cNo")); + lore.add("§7Recipe Level Cost: §e" + recipe.getLevelCostPerCraft()); + lore.add("§7Recipe Linear Xp Cost: §e" + recipe.getXpCostPerCraft()); + if (recipe.getXpCostPerCraft() != 0) { + lore.add("§7Exact Linear xp remove: §" + (recipe.getRemoveExactLinearXp() ? "aYes" : "cNo")); + } + return lore; + } + + @Override + protected LazyElement newInstanceOfGui(AnvilCustomRecipe generic, GuiItem item) { + return new LazyElement<>(item, () -> new CustomRecipeSubSettingGui(this, generic)); + } + + @Override + protected String genericDisplayedName() { + return "custom recipe"; + } + + @Override + protected AnvilCustomRecipe createAndSaveNewEmptyGeneric(String name) { + // Create new empty conflict and display it to the admin + AnvilCustomRecipe recipe = new AnvilCustomRecipe( + name, + AnvilCustomRecipe.DEFAULT_EXACT_COUNT_CONFIG, + + AnvilCustomRecipe.DEFAULT_XP_LEVEL_COST_CONFIG, + AnvilCustomRecipe.DEFAULT_LINEAR_XP_COST_CONFIG, + AnvilCustomRecipe.DEFAULT_REMOVE_EXACT_XP_CONFIG, + + AnvilCustomRecipe.Companion.getDEFAULT_LEFT_ITEM_CONFIG(), + AnvilCustomRecipe.Companion.getDEFAULT_RIGHT_ITEM_CONFIG(), + AnvilCustomRecipe.Companion.getDEFAULT_RESULT_ITEM_CONFIG()); + + ConfigHolder.CUSTOM_RECIPE_HOLDER.getRecipeManager().cleanAddNew(recipe); + + // Save recipe to file + recipe.saveToFile(GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE, GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + + return recipe; + } + + + @Override + protected Collection getEveryDisplayableInstanceOfGeneric() { + return ConfigHolder.CUSTOM_RECIPE_HOLDER.getRecipeManager().getRecipeList(); + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantConflictGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantConflictGui.java new file mode 100644 index 0000000..912e6cb --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantConflictGui.java @@ -0,0 +1,104 @@ +package xyz.alexcrea.cuanvil.gui.config.global; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.group.EnchantConflictGroup; +import xyz.alexcrea.cuanvil.group.IncludeGroup; +import xyz.alexcrea.cuanvil.gui.config.list.MappedGuiListConfigGui; +import xyz.alexcrea.cuanvil.gui.config.list.elements.EnchantConflictSubSettingGui; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.Arrays; +import java.util.Collection; + +public class EnchantConflictGui extends MappedGuiListConfigGui> { + + private static EnchantConflictGui INSTANCE; + + @Nullable + public static EnchantConflictGui getCurrentInstance(){ + return INSTANCE; + } + + @NotNull + public static EnchantConflictGui getInstance(){ + if(INSTANCE == null) INSTANCE = new EnchantConflictGui(); + + return INSTANCE; + } + + + private EnchantConflictGui() { + super( "Conflict Config"); + + init(); + } + + @Override + protected EnchantConflictGroup createAndSaveNewEmptyGeneric(String name){ + // Create new empty conflict and display it to the admin + EnchantConflictGroup conflict = new EnchantConflictGroup( + name, + new IncludeGroup("new_group"), + 0); + + ConfigHolder.CONFLICT_HOLDER.getConflictManager().addConflict(conflict); + + // save empty conflict in config + String[] emptyStringArray = new String[0]; + + FileConfiguration config = ConfigHolder.CONFLICT_HOLDER.getConfig(); + config.set(name + ".enchantments", emptyStringArray); + config.set(name + ".notAffectedGroups", emptyStringArray); + config.set(name + ".maxEnchantmentBeforeConflict", 0); + + if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) { + ConfigHolder.CONFLICT_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + } + + return conflict; + } + + @Override + public ItemStack createItemForGeneric(EnchantConflictGroup conflict) { + ItemStack item = new ItemStack(conflict.getRepresentativeMaterial()); + + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.addItemFlags(ItemFlag.values()); + meta.setDisplayName("§e" + CasedStringUtil.snakeToUpperSpacedCase(conflict.toString()) + " §fConflict"); + meta.setLore(Arrays.asList( + "§7Enchantment count: §e" + conflict.getEnchants().size(), + "§7Group count: §e" + conflict.getCantConflictGroup().getGroups().size(), + "§7Min enchantments count: §e" + conflict.getMinBeforeBlock() + )); + + item.setItemMeta(meta); + return item; + } + + @Override + protected LazyElement newInstanceOfGui(EnchantConflictGroup conflict, GuiItem item) { + return new LazyElement<>(item, () -> new EnchantConflictSubSettingGui(this, conflict)); + } + + @Override + protected String genericDisplayedName() { + return "conflict"; + } + + @Override + protected Collection getEveryDisplayableInstanceOfGeneric() { + return ConfigHolder.CONFLICT_HOLDER.getConflictManager().getConflictList(); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantCostConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantCostConfigGui.java new file mode 100644 index 0000000..a614536 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantCostConfigGui.java @@ -0,0 +1,91 @@ +package xyz.alexcrea.cuanvil.gui.config.global; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.enchant.EnchantmentProperties; +import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity; +import xyz.alexcrea.cuanvil.gui.config.settings.EnchantCostSettingsGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +/** + * Global Config gui for enchantment cost settings. + */ +public class EnchantCostConfigGui extends AbstractEnchantConfigGui { + + private static final String SECTION_NAME = "enchant_values"; + + private static EnchantCostConfigGui INSTANCE = null; + + @Nullable + public static EnchantCostConfigGui getInstance() { + return INSTANCE; + } + + /** + * Constructor of this Global gui for enchantment cost settings. + */ + public EnchantCostConfigGui() { + super("§8Enchantment Level Cost"); + if(INSTANCE == null) INSTANCE = this; + + init(); + } + + @Override + public EnchantCostSettingsGui.EnchantCostSettingFactory createFactory(CAEnchantment enchant) { + String key = enchant.getKey().toString().toLowerCase(Locale.ENGLISH); + String prettyKey = CasedStringUtil.snakeToUpperSpacedCase(key.replace(":", "_")); + + return new EnchantCostSettingsGui.EnchantCostSettingFactory(prettyKey + " Cost", this, + SECTION_NAME + '.' + key, ConfigHolder.DEFAULT_CONFIG, + Arrays.asList( + "§7How many level should " + prettyKey, + "§7cost when applied by book or by another item." + ), + enchant, 0, 255, + 1, 10, 50); + } + + @Override + public GuiItem itemFromFactory(CAEnchantment enchantment, EnchantCostSettingsGui.EnchantCostSettingFactory factory) { + // Get item properties + int itemCost = factory.getConfiguredValue(); + int bookCost = factory.getConfiguredBookValue(); + String itemName = "§a" + factory.getTitle(); + // Create item + ItemStack item = new ItemStack(Material.ENCHANTED_BOOK); + ItemMeta itemMeta = item.getItemMeta(); + assert itemMeta != null; + + // Prepare lore + List lore = new ArrayList<>(); + lore.add("§7Item Cost: §e" + itemCost); + lore.add("§7Book Cost: §e" + bookCost); + + List displayLore = factory.getDisplayLore(); + if(!displayLore.isEmpty()){ + lore.add(""); + lore.addAll(displayLore); + } + + // Edit name and lore + itemMeta.setDisplayName(itemName); + itemMeta.setLore(lore); + + item.setItemMeta(itemMeta); + + return GuiGlobalItems.openSettingGuiItem(item, factory); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantLimitConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantLimitConfigGui.java new file mode 100644 index 0000000..e9edbeb --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantLimitConfigGui.java @@ -0,0 +1,83 @@ +package xyz.alexcrea.cuanvil.gui.config.global; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import io.delilaheve.util.ConfigOptions; +import org.bukkit.Material; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.gui.config.settings.IntSettingsGui; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.Collections; +import java.util.Locale; + +/** + * Global Config gui for enchantment level limit settings. + */ +public class EnchantLimitConfigGui extends AbstractEnchantConfigGui { + + private static final String SECTION_NAME = ConfigOptions.ENCHANT_LIMIT_ROOT; + + private static EnchantLimitConfigGui INSTANCE = null; + + @Nullable + public static EnchantLimitConfigGui getInstance() { + return INSTANCE; + } + + /** + * Constructor of this Global gui for enchantment level limit settings. + */ + public EnchantLimitConfigGui() { + super("§8Enchantment Level Limit"); + if(INSTANCE == null) INSTANCE = this; + + init(); + } + + @Override + public IntSettingsGui.IntSettingFactory createFactory(CAEnchantment enchant) { + String key = enchant.getKey().toString().toLowerCase(Locale.ROOT); + String prettyKey = CasedStringUtil.snakeToUpperSpacedCase(key.replace(":", "_")); + + var defaultValue = enchant.defaultMaxLevel(); + + return new IntSettingsGui.IntSettingFactory(prettyKey + " Limit", this, + SECTION_NAME + '.' + key, ConfigHolder.DEFAULT_CONFIG, + Collections.singletonList( + "§7Maximum applied level of " + prettyKey + ), + -1, 255, -1, + 1, 5, 10, 50, 100){ + + @Override + public int getConfiguredValue() { + var value = ConfigOptions.INSTANCE.rawEnchantLimit(enchant); + return Math.min(value, ConfigOptions.ENCHANT_LIMIT); + } + + @Override + public String valueDisplayName(IntSettingsGui.ValueDisplayType type, int value) { + + if(value < 0) { + return switch (type) { + case CURRENT -> "Default (" + defaultValue + ")"; + case RESET -> String.valueOf(defaultValue); + default -> "Default"; + }; + + } + else return super.valueDisplayName(type, value); + } + }; + } + + @Override + public GuiItem itemFromFactory(CAEnchantment enchantment, IntSettingsGui.IntSettingFactory inventoryFactory) { + return inventoryFactory.getItem( + Material.ENCHANTED_BOOK, + inventoryFactory.getTitle()); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantMergeLimitConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantMergeLimitConfigGui.java new file mode 100644 index 0000000..0d391e7 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantMergeLimitConfigGui.java @@ -0,0 +1,67 @@ +package xyz.alexcrea.cuanvil.gui.config.global; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import io.delilaheve.util.ConfigOptions; +import org.bukkit.Material; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.gui.config.settings.IntSettingsGui; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.Arrays; +import java.util.Locale; + +public class EnchantMergeLimitConfigGui extends AbstractEnchantConfigGui { + + private static final String SECTION_NAME = "disable-merge-over"; + + private static EnchantMergeLimitConfigGui INSTANCE = null; + + @Nullable + public static EnchantMergeLimitConfigGui getInstance() { + return INSTANCE; + } + + /** + * Constructor of this Global gui for enchantment level limit settings. + */ + public EnchantMergeLimitConfigGui() { + super("§8Enchantment Maximum Merge Level"); + if(INSTANCE == null) INSTANCE = this; + + init(); + } + + @Override + public IntSettingsGui.IntSettingFactory createFactory(CAEnchantment enchant) { + String key = enchant.getKey().toString().toLowerCase(Locale.ROOT); + String prettyKey = CasedStringUtil.snakeToUpperSpacedCase(key.replace(":", "_")); + + return new IntSettingsGui.IntSettingFactory(prettyKey + " Merge Limit", this, + SECTION_NAME + '.' + key, ConfigHolder.DEFAULT_CONFIG, + Arrays.asList( + "§7Maximum merge level for for " + prettyKey, + "", + "§7For example, if set to §e2§7, §alvl1 §7+ §alvl1 §7of will give a §alvl2", + "§7But §alvl2 §7+ §alvl2 §7will not give a §clv3§7.", + "§7Will still not merge above max enchantment level", + "§e-1 §7(default) will set the merge limit to enchantment's maximum level" + ), + -1, 255, -1, + 1, 5, 10, 50, 100){ + + @Override + public int getConfiguredValue() { + return ConfigOptions.INSTANCE.maxBeforeMergeDisabled(enchant); + } + }; + } + + @Override + public GuiItem itemFromFactory(CAEnchantment enchantment, IntSettingsGui.IntSettingFactory inventoryFactory) { + return inventoryFactory.getItem( + Material.ENCHANTED_BOOK, + inventoryFactory.getTitle()); + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/GroupConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/GroupConfigGui.java new file mode 100644 index 0000000..8e20751 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/GroupConfigGui.java @@ -0,0 +1,97 @@ +package xyz.alexcrea.cuanvil.gui.config.global; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.group.AbstractMaterialGroup; +import xyz.alexcrea.cuanvil.group.GroupType; +import xyz.alexcrea.cuanvil.group.IncludeGroup; +import xyz.alexcrea.cuanvil.group.ItemGroupManager; +import xyz.alexcrea.cuanvil.gui.config.list.MappedGuiListConfigGui; +import xyz.alexcrea.cuanvil.gui.config.list.elements.GroupConfigSubSettingGui; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; +import xyz.alexcrea.cuanvil.util.LazyValue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +public class GroupConfigGui extends MappedGuiListConfigGui> { + + private static GroupConfigGui INSTANCE; + + @Nullable + public static GroupConfigGui getCurrentInstance(){ + return INSTANCE; + } + + @NotNull + public static GroupConfigGui getInstance(){ + if(INSTANCE == null) INSTANCE = new GroupConfigGui(); + + return INSTANCE; + } + + public GroupConfigGui() { + super("Group Config"); + + init(); + } + + @Override + protected ItemStack createItemForGeneric(IncludeGroup group) { + ItemStack item = new ItemStack(group.getRepresentativeMaterial()); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.addItemFlags(ItemFlag.values()); + meta.setDisplayName("§e" + CasedStringUtil.snakeToUpperSpacedCase(group.getName())+ " §fGroup"); + meta.setLore(Arrays.asList( + "§7Number of selected groups : " + group.getGroups().size(), + "§7Number of included material : " + group.getNonGroupInheritedMaterials().size(), + "", + "§7Total number of included material "+group.getMaterials().size())); + + item.setItemMeta(meta); + return item; + } + + @Override + protected Collection getEveryDisplayableInstanceOfGeneric() { + ArrayList includeGroups = new ArrayList<>(); + + for (AbstractMaterialGroup group : ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager().getGroupMap().values()) { + if(group instanceof IncludeGroup){ + includeGroups.add((IncludeGroup) group); + } + } + return includeGroups; + } + + @Override + protected LazyElement newInstanceOfGui(IncludeGroup group, GuiItem item) { + return new LazyElement<>(item, () -> new GroupConfigSubSettingGui(this, group)); + } + + @Override + protected String genericDisplayedName() { + return "material group"; + } + + @Override + protected IncludeGroup createAndSaveNewEmptyGeneric(String name) { + ItemGroupManager manager = ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager(); + if(manager.getGroupMap().containsKey(name)) return null; + + ConfigurationSection config = ConfigHolder.ITEM_GROUP_HOLDER.getConfig(); + config.set(name+"."+ItemGroupManager.GROUP_TYPE_PATH, GroupType.INCLUDE.getGroupID()); + + return (IncludeGroup) manager.createGroup(config, name); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/UnitRepairConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/UnitRepairConfigGui.java new file mode 100644 index 0000000..0e366ae --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/UnitRepairConfigGui.java @@ -0,0 +1,148 @@ +package xyz.alexcrea.cuanvil.gui.config.global; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.gui.config.ask.SelectItemTypeGui; +import xyz.alexcrea.cuanvil.gui.config.list.MappedGuiListConfigGui; +import xyz.alexcrea.cuanvil.gui.config.list.UnitRepairElementListGui; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +public class UnitRepairConfigGui extends + MappedGuiListConfigGui> { + + private static UnitRepairConfigGui INSTANCE; + + @Nullable + public static UnitRepairConfigGui getCurrentInstance(){ + return INSTANCE; + } + + @NotNull + public static UnitRepairConfigGui getInstance(){ + if(INSTANCE == null) INSTANCE = new UnitRepairConfigGui(); + + return INSTANCE; + } + + private UnitRepairConfigGui() { + super("Unit Repair Config"); + + init(); + } + + @Override + protected LazyElement newInstanceOfGui(Material material, GuiItem item) { + return new LazyElement<>(item, () -> { + UnitRepairElementListGui element = new UnitRepairElementListGui(material, this); + element.init(); + return element; + }); + } + + @Override + protected ItemStack createItemForGeneric(Material material) { + ConfigurationSection materialSection = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig().getConfigurationSection(material.name().toLowerCase()); + String materialName = CasedStringUtil.snakeToUpperSpacedCase(material.name().toLowerCase()); + + if(material.isAir()){ + material = Material.BARRIER; + } + + int reparableItemCount = materialSection == null ? 0 : materialSection.getKeys(false).size(); // Probably an expensive call but... why not + + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§eRepaired by " +materialName); + meta.setLore(Arrays.asList( + "§7There is currently §e" +reparableItemCount+ " §7reparable item with "+materialName, + "§7Click here to open the menu to edit reparable item by " + materialName + )); + + item.setItemMeta(meta); + + return item; + } + + @Override + protected Collection getEveryDisplayableInstanceOfGeneric() { + ArrayList materials = new ArrayList<>(); + + for (String matName : ConfigHolder.UNIT_REPAIR_HOLDER.getConfig().getKeys(false)) { + Material mat = Material.getMaterial(matName.toUpperCase()); + if(mat != null){ + materials.add(mat); + } + } + return materials; + } + + @Override + protected GuiItem prepareCreateNewItem() { + // Create new conflict item + ItemStack createItem = new ItemStack(Material.PAPER); + ItemMeta createMeta = createItem.getItemMeta(); + assert createMeta != null; + + createMeta.setDisplayName("§aSelect a new unit material"); + createMeta.setLore(Arrays.asList( + "§7Select a new unit material to be used.", + "§7You will be asked the material to use." + )); + + createItem.setItemMeta(createMeta); + + return new GuiItem(createItem, clickEvent -> { + clickEvent.setCancelled(true); + + new SelectItemTypeGui( + "Select unit repair item.", + "§7Click here with an item to set the item\n" + + "§7You like to be an unit repair item", + this, + (itemStack, player) -> { + Material type = itemStack.getType(); + // Add new material + updateValueForGeneric(type, true); + + // Display material edit setting + this.elementGuiMap.get(type).get().getMappedGui().show(player); + }, + true + ).show(clickEvent.getWhoClicked()); + }, CustomAnvil.instance); + } + + @NotNull + public LazyElement getInstanceOrCreate(Material mat){ + LazyElement element = this.elementGuiMap.get(mat); + if(element == null){ + updateValueForGeneric(mat, false); + + element = this.elementGuiMap.get(mat); + } + + return element; + } + + @Override // Not used in this implementation. + protected String genericDisplayedName() { + return "this function Should not be used."; + } + @Override // Not used in this implementation. + protected Material createAndSaveNewEmptyGeneric(String name) { + return null; + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/ElementListConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/ElementListConfigGui.java new file mode 100644 index 0000000..f3cc0b4 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/ElementListConfigGui.java @@ -0,0 +1,318 @@ +package xyz.alexcrea.cuanvil.gui.config.list; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.ChestGui; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import com.github.stefvanschie.inventoryframework.pane.Orientable; +import com.github.stefvanschie.inventoryframework.pane.OutlinePane; +import com.github.stefvanschie.inventoryframework.pane.Pane; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.entity.HumanEntity; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.UUID; + +public abstract class ElementListConfigGui< T > extends ChestGui implements ValueUpdatableGui { + + private final String namePrefix; + + protected PatternPane backgroundPane; + + public static final int LIST_FILLER_START_X = 1; + public static final int LIST_FILLER_START_Y = 1; + public static final int LIST_FILLER_LENGTH = 7; + public static final int LIST_FILLER_HEIGHT = 4; + + protected ElementListConfigGui(@NotNull String title, Gui parent) { + super(6, title, CustomAnvil.instance); + this.namePrefix = title; + + // Back item panel + Pattern pattern = getBackgroundPattern(); + this.backgroundPane = new PatternPane(0, 0, 9, 6, Pane.Priority.LOW, pattern); + GuiGlobalItems.addBackItem(this.backgroundPane, parent); + + } + + protected Pattern getBackgroundPattern(){ + return new Pattern( + GuiSharedConstant.UPPER_FILLER_FULL_PLANE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + "B11L1R11C" + ); + } + + protected OutlinePane firstPage; + protected ArrayList pages; + protected HashMap pageMap; + + public void init() { // Why I'm using an init function ? //TODO determine why is it using a init function and not used on constructor. + GuiGlobalItems.addBackgroundItem(this.backgroundPane); + this.backgroundPane.bindItem('1', GuiSharedConstant.SECONDARY_BACKGROUND_ITEM); + addPane(this.backgroundPane); + + // Page init + this.pages = new ArrayList<>(); + this.pageMap = new HashMap<>(); + + // enchant item panel + this.firstPage = createEmptyPage(); + this.pages.add(this.firstPage); + + prepareStaticValues(); + reloadValues(); + } + + protected GuiItem goLeftItem; + protected GuiItem goRightItem; + + protected void prepareStaticValues(){ + // Left item creation for consumer & bind + this.goLeftItem = new GuiItem(new ItemStack(Material.RED_STAINED_GLASS_PANE), event -> { + HumanEntity viewer = event.getWhoClicked(); + UUID playerUUID = viewer.getUniqueId(); + int page = this.pageMap.getOrDefault(playerUUID, 0); + this.pageMap.put(playerUUID, page - 1); + + ItemStack cursor = viewer.getItemOnCursor(); + viewer.setItemOnCursor(new ItemStack(Material.AIR)); + + show(viewer); + + viewer.setItemOnCursor(cursor); + }, CustomAnvil.instance); + + // Right item creation for consumer & bind + this.goRightItem = new GuiItem(new ItemStack(Material.LIME_STAINED_GLASS_PANE), event -> { + HumanEntity viewer = event.getWhoClicked(); + UUID playerUUID = viewer.getUniqueId(); + int page = pageMap.getOrDefault(playerUUID, 0); + this.pageMap.put(playerUUID, page + 1); + + ItemStack cursor = viewer.getItemOnCursor(); + viewer.setItemOnCursor(new ItemStack(Material.AIR)); + + show(viewer); + + viewer.setItemOnCursor(cursor); + }, CustomAnvil.instance); + + GuiItem createNew = prepareCreateNewItem(); + if(createNew != null){ + this.backgroundPane.bindItem('C', createNew); + } + } + protected void reloadValues(){ + this.firstPage.clear(); + this.pages.clear(); + this.pages.add(this.firstPage); + + for (T generic : getEveryDisplayableInstanceOfGeneric()) { + updateValueForGeneric(generic, false); + } + + update(); + } + + protected abstract GuiItem prepareCreateNewItem(); + + protected OutlinePane createEmptyPage() { + OutlinePane page = new OutlinePane(LIST_FILLER_START_X, LIST_FILLER_START_Y, LIST_FILLER_LENGTH, LIST_FILLER_HEIGHT); + page.align(OutlinePane.Alignment.BEGIN); + page.setOrientation(Orientable.Orientation.HORIZONTAL); + + return page; + } + + public int getPlayerPageID(UUID uuid) { + int pageId = this.pageMap.getOrDefault(uuid, 0); + if (pageId >= this.pages.size()) { + pageId = this.pages.size() - 1; + } + return pageId; + } + + protected void addToPage(GuiItem guiItem) { + // Get first available page or create one + OutlinePane page = this.pages.get(this.pages.size() - 1); + if (page.getItems().size() >= LIST_FILLER_LENGTH * LIST_FILLER_HEIGHT) { + page = createEmptyPage(); + this.pages.add(page); + } + + page.addItem(guiItem); + } + + private void removeFromPage(GuiItem guiItem) { + // get item page + OutlinePane page = null; + int pageID = 0; + while (pageID < this.pages.size()) { + OutlinePane tempPage = this.pages.get(pageID); + if (tempPage.getItems().contains(guiItem)) { + page = tempPage; + break; + } + pageID++; + } + + if (page == null) {// Why... + return; + } + removeFromPage(page, pageID, guiItem); + } + + private void removeFromPage(OutlinePane page, int pageID, GuiItem guiItem) { + page.removeItem(guiItem); + + // There is now a slot available, let fill it if possible + if (pageID < (this.pages.size() - 1)) { + OutlinePane newPage = this.pages.get(pageID + 1); + GuiItem nextPageItem = newPage.getItems().get(0); + + removeFromPage(newPage, pageID + 1, nextPageItem); + + OutlinePane thisPage = this.pages.get(pageID); + thisPage.addItem(nextPageItem); + } else if (pageID > 0 && page.getItems().isEmpty()) { + this.pages.remove(pageID); + } + } + + public void placeArrow(int page, boolean customise) { + + // Place left arrow + addPane(this.backgroundPane); + if (page > 0) { + if (customise) { + ItemStack leftItem = this.goLeftItem.getItem(); + ItemMeta leftMeta = leftItem.getItemMeta(); + + leftMeta.setDisplayName("§eReturn to page " + (page)); + + leftItem.setItemMeta(leftMeta); + this.goLeftItem.setItem(leftItem); + } + + this.backgroundPane.bindItem('L', this.goLeftItem); + } else { + this.backgroundPane.bindItem('L', GuiSharedConstant.SECONDARY_BACKGROUND_ITEM); + } + + // Place right arrow + if (page < pages.size() - 1) { + if (customise) { + ItemStack rightItem = this.goRightItem.getItem(); + ItemMeta rightMeta = rightItem.getItemMeta(); + + rightMeta.setDisplayName("§eGo to page " + (page + 2)); + + rightItem.setItemMeta(rightMeta); + this.goRightItem.setItem(rightItem); + } + + this.backgroundPane.bindItem('R', this.goRightItem); + } else { + this.backgroundPane.bindItem('R', GuiSharedConstant.SECONDARY_BACKGROUND_ITEM); + } + } + + @Override // assume will not be called in multiple thread + public void show(@NotNull HumanEntity humanEntity) { + int pageID = getPlayerPageID(humanEntity.getUniqueId()); + OutlinePane page = this.pages.get(pageID); + + getPanes().clear(); + + // display the page arrow pane + placeArrow(pageID, true); + // and add actual page + addPane(page); + + // set title + StringBuilder title = new StringBuilder(this.namePrefix); + int pagesSize = this.pages.size(); + if(pagesSize > 1){ + title.append(" (").append(pageID + 1).append('/').append(pagesSize).append(')'); + } + setTitle(title.toString()); + + super.show(humanEntity); + + } + + @Override // assume will not be called in multiple thread + public void click(@NotNull InventoryClickEvent event) { + int pageID = getPlayerPageID(event.getWhoClicked().getUniqueId()); + OutlinePane page = this.pages.get(pageID); + + getPanes().clear(); + + // set the page arrow pane + placeArrow(pageID, false); + // and add actual page + addPane(page); + + super.click(event); + } + + // ------------------------- + // Methods using generic T + // ------------------------- + + public void updateValueForGeneric(T generic, boolean shouldUpdate) { + ItemStack item = createItemForGeneric(generic); + + updateGeneric(generic, item); + + if (shouldUpdate) { + update(); + } + + } + + public void removeGeneric(T generic) { + GuiItem item = findGuiItemForRemoval(generic); + if(item == null) return; + removeFromPage(item); + + update(); + } + + protected abstract GuiItem findGuiItemForRemoval(T generic); + + protected abstract ItemStack createItemForGeneric(T generic); + + protected abstract void updateGeneric(T generic, ItemStack usedItem); + + protected abstract Collection getEveryDisplayableInstanceOfGeneric(); + + @Override + public void updateGuiValues() { + // Not the optimised way to update this gui + // TODO maybe rework ValueUpdatableGui and it's dependency to allow a 1 item reload every time. + + reloadValues(); + } + + @Override + public Gui getConnectedGui() { + return this; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/MappedElementListConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/MappedElementListConfigGui.java new file mode 100644 index 0000000..31ab718 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/MappedElementListConfigGui.java @@ -0,0 +1,106 @@ +package xyz.alexcrea.cuanvil.gui.config.list; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.entity.HumanEntity; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.gui.config.MainConfigGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.function.Consumer; + +public abstract class MappedElementListConfigGui< T, S > extends ElementListConfigGui< T > { + + protected final HashMap elementGuiMap; + protected MappedElementListConfigGui(@NotNull String title) { + super(title, MainConfigGui.getInstance()); + this.elementGuiMap = new HashMap<>(); + + } + + @Override + protected GuiItem prepareCreateNewItem(){ + // Create new conflict item + ItemStack createItem = new ItemStack(Material.PAPER); + ItemMeta createMeta = createItem.getItemMeta(); + assert createMeta != null; + + createMeta.setDisplayName("§aCreate new "+genericDisplayedName()); + createMeta.setLore(Arrays.asList( + "§7Create a new "+genericDisplayedName()+".", + "§7You will be asked to name the "+genericDisplayedName()+" in chat.", + "§7Then, you should edit the "+genericDisplayedName()+" config as you need" + )); + + createItem.setItemMeta(createMeta); + + return new GuiItem(createItem, clickEvent -> { + clickEvent.setCancelled(true); + HumanEntity player = clickEvent.getWhoClicked(); + + // check permission + if (!player.hasPermission(CustomAnvil.editConfigPermission)) { + player.closeInventory(); + player.sendMessage(GuiGlobalActions.NO_EDIT_PERM); + return; + } + player.closeInventory(); + + player.sendMessage("§eWrite the "+genericDisplayedName()+" name you want to create in the chat.\n" + + "§eOr write §ccancel §eto go back to "+genericDisplayedName()+" config menu"); + + CustomAnvil.Companion.getChatListener().setListenedCallback(player, prepareCreateItemConsumer(player)); + + }, CustomAnvil.instance); + } + + @Override + protected void updateGeneric(T generic, ItemStack usedItem) { + S element = this.elementGuiMap.get(generic); + + GuiItem guiItem; + if (element == null) { + // Create new sub setting element + guiItem = new GuiItem(usedItem, CustomAnvil.instance); + + element = newElementRequested(generic, guiItem); + + this.elementGuiMap.put(generic, element); + + addToPage(guiItem); + } else { + // Replace item with the updated one + guiItem = findItemFromElement(generic, element); + guiItem.setItem(usedItem); + } + updateElement(generic, element); + + } + + protected abstract void updateElement(T generic, S element); + + protected abstract S newElementRequested(T generic, GuiItem newItem); + + protected abstract GuiItem findItemFromElement(T generic, S element); + + @Override + protected GuiItem findGuiItemForRemoval(T generic) { + S element = this.elementGuiMap.get(generic); + if (element == null) return null; + + this.elementGuiMap.remove(generic); + return findGuiItemForRemoval(generic, element); + } + + protected abstract GuiItem findGuiItemForRemoval(T generic, S element); + + protected abstract Consumer prepareCreateItemConsumer(HumanEntity player); + + protected abstract String genericDisplayedName(); + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/MappedGuiListConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/MappedGuiListConfigGui.java new file mode 100644 index 0000000..3aa18e0 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/MappedGuiListConfigGui.java @@ -0,0 +1,140 @@ +package xyz.alexcrea.cuanvil.gui.config.list; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import io.delilaheve.CustomAnvil; +import org.bukkit.entity.HumanEntity; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.gui.config.list.elements.ElementMappedToListGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions; +import xyz.alexcrea.cuanvil.util.LazyValue; + +import java.util.Locale; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public abstract class MappedGuiListConfigGui< T, S extends MappedGuiListConfigGui.LazyElement> + extends MappedElementListConfigGui< T, S > { + + protected MappedGuiListConfigGui(@NotNull String title) { + super(title); + + } + + @Override + public void reloadValues() { + this.elementGuiMap.forEach((conflict, element) -> { + ElementMappedToListGui gui = element.getStored(); + if(gui != null) gui.cleanAndBeUnusable(); + }); + this.elementGuiMap.clear(); + + super.reloadValues(); + } + + @Override + protected S newElementRequested(T generic, GuiItem newItem) { + S element = newInstanceOfGui(generic, newItem); + + newItem.setAction(element.openAction()); + return element; + } + + @Override + protected GuiItem findItemFromElement(T generic, S element) { + return element.getParentItem(); + } + + @Override + protected void updateElement(T generic, S element) { + ElementMappedToListGui gui = element.getStored(); + if(gui != null) gui.updateLocal(); + } + + @Override + protected GuiItem findGuiItemForRemoval(T generic, S element) { + return element.getParentItem(); + } + + @Override + protected Consumer prepareCreateItemConsumer(HumanEntity player){ + AtomicReference> selfRef = new AtomicReference<>(); + Consumer selfCallback = (message) -> { + if (message == null) return; + + // check permission + if (!player.hasPermission(CustomAnvil.editConfigPermission)) { + player.sendMessage(GuiGlobalActions.NO_EDIT_PERM); + return; + } + + message = message.toLowerCase(Locale.ROOT); + if ("cancel".equalsIgnoreCase(message)) { + player.sendMessage(genericDisplayedName()+" creation cancelled..."); + show(player); + return; + } + + message = message.replace(' ', '_'); + + // Try to find if it already exists in a for loop + // Not the most efficient on large number of conflict, but it should not run often. + for (T generic : getEveryDisplayableInstanceOfGeneric()) { + if (generic.toString().equalsIgnoreCase(message)) { + player.sendMessage("§cPlease enter a "+genericDisplayedName()+" name that do not already exist..."); + // wait next message. + CustomAnvil.Companion.getChatListener().setListenedCallback(player, selfRef.get()); + return; + } + } + + T generic = createAndSaveNewEmptyGeneric(message); + if(generic == null) {// we don't know what to do. so we back up by opening this gui. + this.show(player); + return; + } + + updateValueForGeneric(generic, true); + + // show the new conflict config to the player + this.elementGuiMap.get(generic).get().getMappedGui().show(player); + + update(); + }; + + selfRef.set(selfCallback); + return selfCallback; + } + + protected abstract S newInstanceOfGui(T generic, GuiItem item); + + protected abstract String genericDisplayedName(); + + protected abstract T createAndSaveNewEmptyGeneric(String name); + + public static class LazyElement extends LazyValue { + + private final GuiItem parentItem; + private final LazyValue> lazyOpenConsumer; + public LazyElement(GuiItem parentItem, Supplier valueSupplier) { + super(valueSupplier); + this.parentItem = parentItem; + + this.lazyOpenConsumer = new LazyValue<>(() -> + GuiGlobalActions.openGuiAction(this.get().getMappedGui())) + ; + } + + public GuiItem getParentItem() { + return parentItem; + } + + @NotNull + public Consumer openAction(){ + return event -> lazyOpenConsumer.get().accept(event); + } + + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/SettingGuiListConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/SettingGuiListConfigGui.java new file mode 100644 index 0000000..f0e7cb0 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/SettingGuiListConfigGui.java @@ -0,0 +1,103 @@ +package xyz.alexcrea.cuanvil.gui.config.list; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.gui.config.MainConfigGui; +import xyz.alexcrea.cuanvil.gui.config.settings.SettingGui; + +import java.util.HashMap; +import java.util.List; +import java.util.function.Consumer; + +public abstract class SettingGuiListConfigGui< T, S extends SettingGui.SettingGuiFactory> extends ElementListConfigGui< T >{ + + protected HashMap guiItemMap; + protected HashMap factoryMap; + protected SettingGuiListConfigGui(@NotNull String title, Gui parent) { + super(title, parent); + this.guiItemMap = new HashMap<>(); + this.factoryMap = new HashMap<>(); + } + + protected SettingGuiListConfigGui(@NotNull String title) { + this(title, MainConfigGui.getInstance()); + } + + @Override + protected GuiItem prepareCreateNewItem() { + ItemStack createItem = new ItemStack(Material.PAPER); + ItemMeta createMeta = createItem.getItemMeta(); + assert createMeta != null; + + createMeta.setDisplayName(createItemName()); + createMeta.setLore(getCreateItemLore()); + + createItem.setItemMeta(createMeta); + return new GuiItem(createItem, getCreateClickConsumer(), CustomAnvil.instance); + } + + @Override + public void updateValueForGeneric(T generic, boolean shouldUpdate) { + if(!this.factoryMap.containsKey(generic)){ + // Create new item & factory + S factory = createFactory(generic); + GuiItem newItem = itemFromFactory(generic, factory); + + addToPage(newItem); + this.guiItemMap.put(generic, newItem); + this.factoryMap.put(generic, factory); + }else{ + S factory = this.factoryMap.get(generic); + // Update old item + GuiItem oldItem = this.guiItemMap.get(generic); + + GuiItem newItem = itemFromFactory(generic, factory); + updateGuiItem(oldItem, newItem); + } + + if(shouldUpdate){ + update(); + } + } + + @Override + protected void reloadValues() { + this.guiItemMap.clear(); + this.factoryMap.clear(); + + super.reloadValues(); + } + + private void updateGuiItem(GuiItem oldITem, GuiItem newItem){ + oldITem.setItem(newItem.getItem()); + oldITem.setProperties(newItem.getProperties()); + oldITem.setVisible(newItem.isVisible()); + } + + @Override + protected GuiItem findGuiItemForRemoval(T generic) { + return this.guiItemMap.get(generic); + } + + @Override // Not used + protected void updateGeneric(T generic, ItemStack usedItem) {} + @Override // Not used + protected ItemStack createItemForGeneric(T generic) { + return null; + } + + protected abstract List getCreateItemLore(); + protected abstract Consumer getCreateClickConsumer(); + protected abstract String createItemName(); + + protected abstract S createFactory(T generic); + protected abstract GuiItem itemFromFactory(T generic, S factory); + + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/UnitRepairElementListGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/UnitRepairElementListGui.java new file mode 100644 index 0000000..35f8ebb --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/UnitRepairElementListGui.java @@ -0,0 +1,195 @@ +package xyz.alexcrea.cuanvil.gui.config.list; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.HumanEntity; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.meta.Damageable; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.gui.config.ask.SelectItemTypeGui; +import xyz.alexcrea.cuanvil.gui.config.global.UnitRepairConfigGui; +import xyz.alexcrea.cuanvil.gui.config.list.elements.ElementMappedToListGui; +import xyz.alexcrea.cuanvil.gui.config.settings.DoubleSettingGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +public class UnitRepairElementListGui extends SettingGuiListConfigGui implements ElementMappedToListGui { + + private final Material parentMaterial; + private final UnitRepairConfigGui parentGui; + private final String materialName; + + private boolean shouldWork = true; + public UnitRepairElementListGui(@NotNull Material parentMaterial, + @NotNull UnitRepairConfigGui parentGui) { + super("§e" + CasedStringUtil.snakeToUpperSpacedCase(parentMaterial.name().toLowerCase()) + " §rUnit repair"); + this.parentMaterial = parentMaterial; + this.parentGui = parentGui; + this.materialName = CasedStringUtil.snakeToUpperSpacedCase(parentMaterial.name().toLowerCase()); + + GuiGlobalItems.addBackItem(this.backgroundPane, parentGui); + } + + // SettingGuiListConfigGui methods + @Override + protected List getCreateItemLore() { + return Arrays.asList( + "§7Select a new item to be repairable.", + "§7You will be asked the material to use." + ); + } + + @Override + protected Consumer getCreateClickConsumer() { + return event -> { + event.setCancelled(true); + if(!this.shouldWork){ + return; + } + event.setCancelled(true); + + new SelectItemTypeGui( + "Select item to be repaired.", + "§7Click here with an item to set the item\n" + + "§7You like to be repaired by " + this.materialName, + this, + (itemStack, player) -> { + ItemMeta meta = itemStack.getItemMeta(); + Material type = itemStack.getType(); + + if(!(meta instanceof Damageable) || (type.getMaxDurability() <= 0)) { + player.sendMessage("§cThis item can't be damaged, so it can't be repaired."); + return; + } + if(type == this.parentMaterial){ + player.sendMessage("§cItem can't repair something of the same type."); + return; + } + + String materialName = type.name().toLowerCase(); + + // Add new material + ConfigHolder.UNIT_REPAIR_HOLDER.getConfig().set(parentMaterial.name().toLowerCase() + "." + materialName,0.25); + + if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) { + ConfigHolder.UNIT_REPAIR_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + } + + // Update gui + updateValueForGeneric(materialName, true); + this.parentGui.updateValueForGeneric(this.parentMaterial, true); + + + // Display material edit setting + this.factoryMap.get(materialName).create().show(player); + }, + true + ).show(event.getWhoClicked()); + + }; + } + + @Override + protected String createItemName() { + return "§aAdd a new item reparable by " + this.materialName; + } + + @Override + protected DoubleSettingGui.DoubleSettingFactory createFactory(String materialName) { + String materialDisplayName = CasedStringUtil.snakeToUpperSpacedCase(materialName); + + return new DoubleSettingGui.DoubleSettingFactory( + "§0%§8" + materialDisplayName +" Repair", + this, + ConfigHolder.UNIT_REPAIR_HOLDER, + this.parentMaterial.name().toLowerCase()+"."+materialName, + Arrays.asList( + "§7Click here to change how many §e% §7of §a" + materialDisplayName, + "§7Should get repaired by §e"+this.materialName + ), + 2, + true, true, + 0, + 1, + 0.25, + 0.01, 0.05, 0.25 + ); + } + + @Override + protected GuiItem itemFromFactory(String materialName, DoubleSettingGui.DoubleSettingFactory factory) { + return factory.getItem(materialFromName(materialName), + "§7%§a" + CasedStringUtil.snakeToUpperSpacedCase(materialName)+ " §erepaired by §a" + this.materialName); + } + + @Override + protected Collection getEveryDisplayableInstanceOfGeneric() { + ArrayList keys = new ArrayList<>(); + if(!this.shouldWork){ + return keys; + } + + ConfigurationSection materialSection = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig().getConfigurationSection(parentMaterial.name().toLowerCase()); + if(materialSection == null){ + return keys; + } + keys.addAll(materialSection.getKeys(false)); + return keys; + } + + private Material materialFromName(String materialName){ + Material mat = Material.getMaterial(materialName.toUpperCase()); + if(mat == null || mat.isAir()) return Material.BARRIER; + return mat; + } + + @Override + public void updateGuiValues() { + super.updateGuiValues(); + this.parentGui.updateValueForGeneric(this.parentMaterial, true); + } + + // ElementMappedToListGui methods + + @Override // Not used in this implementation + public void updateLocal() {} + + @Override + public void cleanAndBeUnusable() { + this.shouldWork = false; + this.backgroundPane.bindItem('S', GuiGlobalItems.backgroundItem(Material.BLACK_STAINED_GLASS_PANE)); + this.backgroundPane.bindItem('L', GuiGlobalItems.backgroundItem(Material.BLACK_STAINED_GLASS_PANE)); + this.backgroundPane.bindItem('R', GuiGlobalItems.backgroundItem(Material.BLACK_STAINED_GLASS_PANE)); + + for (HumanEntity viewer : getViewers()) { + viewer.sendMessage("This config do not exist anymore"); + this.parentGui.show(viewer); + } + } + + @Override + public Gui getMappedGui() { + return this; + } + + @Override + public void show(@NotNull HumanEntity humanEntity) { + if(!this.shouldWork){ + humanEntity.closeInventory(); + return; + } + super.show(humanEntity); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/CustomRecipeSubSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/CustomRecipeSubSettingGui.java new file mode 100644 index 0000000..9db6d62 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/CustomRecipeSubSettingGui.java @@ -0,0 +1,238 @@ +package xyz.alexcrea.cuanvil.gui.config.list.elements; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import kotlin.ranges.IntRange; +import org.bukkit.Material; +import org.bukkit.entity.HumanEntity; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.gui.config.ask.ConfirmActionGui; +import xyz.alexcrea.cuanvil.gui.config.global.CustomRecipeConfigGui; +import xyz.alexcrea.cuanvil.gui.config.settings.BoolSettingsGui; +import xyz.alexcrea.cuanvil.gui.config.settings.IntSettingsGui; +import xyz.alexcrea.cuanvil.gui.config.settings.ItemSettingGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; +import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe; +import xyz.alexcrea.cuanvil.recipe.CustomAnvilRecipeManager; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.Collections; +import java.util.function.Supplier; + +public class CustomRecipeSubSettingGui extends MappedToListSubSettingGui { + + private final CustomRecipeConfigGui parent; + private final AnvilCustomRecipe anvilRecipe; + private final PatternPane pane; + private boolean shouldWork = true; + + public CustomRecipeSubSettingGui( + @NotNull CustomRecipeConfigGui parent, + @NotNull AnvilCustomRecipe anvilRecipe) { + super(4, "§e" + CasedStringUtil.snakeToUpperSpacedCase(anvilRecipe.toString()) + " §8Config"); + this.parent = parent; + this.anvilRecipe = anvilRecipe; + + Pattern pattern = new Pattern( + GuiSharedConstant.EMPTY_GUI_FULL_LINE, + "01203450D", + "0ab000000", + "B00000000" + ); + this.pane = new PatternPane(0, 0, 9, 4, pattern); + addPane(this.pane); + + prepareStaticValues(); + } + + private BoolSettingsGui.BoolSettingFactory exactCountFactory; + private BoolSettingsGui.BoolSettingFactory removeExactLinearXpFactory; + private GuiItem noRemoveExactLinearXp; + + private IntSettingsGui.IntSettingFactory levelCostFactory; + private IntSettingsGui.IntSettingFactory linearXpCostFactory; + + private ItemSettingGui.ItemSettingFactory leftItemFactory; + private ItemSettingGui.ItemSettingFactory rightItemFactory; + private ItemSettingGui.ItemSettingFactory resultItemFactory; + + private void prepareStaticValues() { + + GuiGlobalItems.addBackItem(this.pane, this.parent); + GuiGlobalItems.addBackgroundItem(this.pane); + + // Delete item + ItemStack deleteItem = new ItemStack(Material.RED_TERRACOTTA); + ItemMeta deleteMeta = deleteItem.getItemMeta(); + assert deleteMeta != null; + + deleteMeta.setDisplayName("§4DELETE RECIPE"); + deleteMeta.setLore(Collections.singletonList("§cCaution with this button !")); + + deleteItem.setItemMeta(deleteMeta); + this.pane.bindItem('D', new GuiItem(deleteItem, GuiGlobalActions.openGuiAction(createDeleteGui()), CustomAnvil.instance)); + + // Displayed item will be updated later + IntRange costRange = AnvilCustomRecipe.Companion.getXP_COST_CONFIG_RANGE(); + this.exactCountFactory = new BoolSettingsGui.BoolSettingFactory("§8Exact count ?", this, + ConfigHolder.CUSTOM_RECIPE_HOLDER, + this.anvilRecipe + "." + AnvilCustomRecipe.EXACT_COUNT_CONFIG, AnvilCustomRecipe.DEFAULT_EXACT_COUNT_CONFIG); + + this.removeExactLinearXpFactory = new BoolSettingsGui.BoolSettingFactory("§8Remove exact linear xp ?", this, + ConfigHolder.CUSTOM_RECIPE_HOLDER, + this.anvilRecipe + "." + AnvilCustomRecipe.REMOVE_EXACT_XP_CONFIG, AnvilCustomRecipe.DEFAULT_REMOVE_EXACT_XP_CONFIG); + + ItemStack item = new ItemStack(Material.BARRIER); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§cRemove exact linear xp ?"); + meta.setLore(Collections.singletonList("§7Not usable if linear cost is 0")); + item.setItemMeta(meta); + this.noRemoveExactLinearXp = new GuiItem(item, GuiGlobalActions.stayInPlace, CustomAnvil.instance); + + this.levelCostFactory = new IntSettingsGui.IntSettingFactory("§8Recipe Level Cost", this, + this.anvilRecipe + "." + AnvilCustomRecipe.XP_LEVEL_COST_CONFIG, + ConfigHolder.CUSTOM_RECIPE_HOLDER, + null, + costRange.getFirst(), costRange.getLast(), AnvilCustomRecipe.DEFAULT_XP_LEVEL_COST_CONFIG, 1, 5, 10); + + this.linearXpCostFactory = new IntSettingsGui.IntSettingFactory("§8Recipe Linear Xp Cost", this, + this.anvilRecipe + "." + AnvilCustomRecipe.LINEAR_XP_COST_CONFIG, + ConfigHolder.CUSTOM_RECIPE_HOLDER, + null, + 0, Integer.MAX_VALUE, AnvilCustomRecipe.DEFAULT_LINEAR_XP_COST_CONFIG, 1, 10, 100, 1000, 10000); + + + // Right part of the gui + this.leftItemFactory = new ItemSettingGui.ItemSettingFactory("§eRecipe Left §8Item", this, + this.anvilRecipe + "." + AnvilCustomRecipe.LEFT_ITEM_CONFIG, + ConfigHolder.CUSTOM_RECIPE_HOLDER, + AnvilCustomRecipe.Companion.getDEFAULT_LEFT_ITEM_CONFIG(), + "§7Set the left item of the custom craft", + "§7\u25A0 + \u25A1 = \u25A1"); + + this.rightItemFactory = new ItemSettingGui.ItemSettingFactory("§eRecipe Right §8Item", this, + this.anvilRecipe + "." + AnvilCustomRecipe.RIGHT_ITEM_CONFIG, + ConfigHolder.CUSTOM_RECIPE_HOLDER, + AnvilCustomRecipe.Companion.getDEFAULT_RIGHT_ITEM_CONFIG(), + "§7Set the right item of the custom craft", + "§7\u25A1 + \u25A0 = \u25A1"); + + this.resultItemFactory = new ItemSettingGui.ItemSettingFactory("§aRecipe Result §8Item", this, + this.anvilRecipe + "." + AnvilCustomRecipe.RESULT_ITEM_CONFIG, + ConfigHolder.CUSTOM_RECIPE_HOLDER, + AnvilCustomRecipe.Companion.getDEFAULT_RESULT_ITEM_CONFIG(), + "§7Set the result item of the custom craft", + "§7\u25A1 + \u25A1 = \u25A0"); + + // Now we update the items + updateLocal(); + } + + private ConfirmActionGui createDeleteGui() { + Supplier deleteSupplier = () -> { + CustomAnvilRecipeManager manager = ConfigHolder.CUSTOM_RECIPE_HOLDER.getRecipeManager(); + + // Remove from manager + manager.cleanRemove(this.anvilRecipe); + + // Remove from parent + this.parent.removeGeneric(this.anvilRecipe); + + // Remove self + cleanAndBeUnusable(); + + // Update config file storage + ConfigHolder.CUSTOM_RECIPE_HOLDER.delete(this.anvilRecipe.toString()); + + // Save + boolean success = true; + if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) { + success = ConfigHolder.CONFLICT_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + } + + return success; + }; + + return new ConfirmActionGui("§cDelete §e" + CasedStringUtil.snakeToUpperSpacedCase(this.anvilRecipe.toString()) + "§c?", + "§7Confirm that you want to delete this conflict.", + this, this.parent, deleteSupplier + ); + } + + @Override + public void updateGuiValues() { + // update value from config to conflict + this.anvilRecipe.updateFromFile(); + + // Parent should call updateLocal with this call + this.parent.updateValueForGeneric(this.anvilRecipe, true); + } + + public void updateLocal() { + if (!this.shouldWork) return; + + GuiItem exactCountItem = this.exactCountFactory.getItem(); + this.pane.bindItem('1', exactCountItem); + + if (anvilRecipe.getXpCostPerCraft() == 0) { + this.pane.bindItem('a', noRemoveExactLinearXp); + } else { + this.pane.bindItem('a', removeExactLinearXpFactory.getItem()); + } + + GuiItem levelCostItem = this.levelCostFactory.getItem(Material.EXPERIENCE_BOTTLE); + this.pane.bindItem('2', levelCostItem); + + + GuiItem xpCostItem = this.linearXpCostFactory.getItem(Material.EXPERIENCE_BOTTLE); + this.pane.bindItem('b', xpCostItem); + + GuiItem leftGuiItem = this.leftItemFactory.getItem(); + this.pane.bindItem('3', leftGuiItem); + + GuiItem rightGuiItem = this.rightItemFactory.getItem(); + this.pane.bindItem('4', rightGuiItem); + + GuiItem resultGuiItem = this.resultItemFactory.getItem(); + this.pane.bindItem('5', resultGuiItem); + + update(); + } + + public void cleanAndBeUnusable() { + for (HumanEntity viewer : getViewers()) { + this.parent.show(viewer); + } + this.shouldWork = false; + + // Just in case something is extremely wrong + GuiItem background = GuiGlobalItems.backgroundItem(); + this.pane.bindItem('1', background); + this.pane.bindItem('2', background); + this.pane.bindItem('3', background); + this.pane.bindItem('4', background); + this.pane.bindItem('5', background); + + this.pane.bindItem('D', background); + } + + @Override + public void show(@NotNull HumanEntity humanEntity) { + if (this.shouldWork) { + super.show(humanEntity); + } else { + this.parent.show(humanEntity); + } + } + + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/ElementMappedToListGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/ElementMappedToListGui.java new file mode 100644 index 0000000..637f72a --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/ElementMappedToListGui.java @@ -0,0 +1,13 @@ +package xyz.alexcrea.cuanvil.gui.config.list.elements; + +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; + +public interface ElementMappedToListGui { + + void updateLocal(); + + void cleanAndBeUnusable(); + + Gui getMappedGui(); + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/EnchantConflictSubSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/EnchantConflictSubSettingGui.java new file mode 100644 index 0000000..97bdfcb --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/EnchantConflictSubSettingGui.java @@ -0,0 +1,330 @@ +package xyz.alexcrea.cuanvil.gui.config.list.elements; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.entity.HumanEntity; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.group.AbstractMaterialGroup; +import xyz.alexcrea.cuanvil.group.EnchantConflictGroup; +import xyz.alexcrea.cuanvil.group.EnchantConflictManager; +import xyz.alexcrea.cuanvil.gui.config.SelectEnchantmentContainer; +import xyz.alexcrea.cuanvil.gui.config.SelectGroupContainer; +import xyz.alexcrea.cuanvil.gui.config.ask.ConfirmActionGui; +import xyz.alexcrea.cuanvil.gui.config.global.EnchantConflictGui; +import xyz.alexcrea.cuanvil.gui.config.settings.EnchantSelectSettingGui; +import xyz.alexcrea.cuanvil.gui.config.settings.GroupSelectSettingGui; +import xyz.alexcrea.cuanvil.gui.config.settings.IntSettingsGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; +import xyz.alexcrea.cuanvil.util.MetricsUtil; + +import java.util.*; +import java.util.function.Supplier; +import java.util.logging.Level; + +public class EnchantConflictSubSettingGui extends MappedToListSubSettingGui implements SelectEnchantmentContainer, SelectGroupContainer { + + private final EnchantConflictGui parent; + private final EnchantConflictGroup enchantConflict; + private final PatternPane pane; + private boolean shouldWork = true; + + public EnchantConflictSubSettingGui( + @NotNull EnchantConflictGui parent, + @NotNull EnchantConflictGroup enchantConflict) { + super(3, + "§e" + CasedStringUtil.snakeToUpperSpacedCase(enchantConflict.toString()) + " §8Config"); + this.parent = parent; + this.enchantConflict = enchantConflict; + + Pattern pattern = new Pattern( + GuiSharedConstant.EMPTY_GUI_FULL_LINE, + "00EGM000D", + "B00000000" + ); + this.pane = new PatternPane(0, 0, 9, 3, pattern); + addPane(this.pane); + + prepareStaticValues(); + } + + private GuiItem enchantSettingItem; + private GuiItem groupSettingItem; + private IntSettingsGui.IntSettingFactory minBeforeActiveSettingFactory; + + private void prepareStaticValues() { + + GuiGlobalItems.addBackItem(this.pane, this.parent); + GuiGlobalItems.addBackgroundItem(this.pane); + + // Delete item + ItemStack deleteItem = new ItemStack(Material.RED_TERRACOTTA); + ItemMeta deleteMeta = deleteItem.getItemMeta(); + assert deleteMeta != null; + + deleteMeta.setDisplayName("§4DELETE CONFLICT"); + deleteMeta.setLore(Collections.singletonList("§cCaution with this button !")); + + deleteItem.setItemMeta(deleteMeta); + this.pane.bindItem('D', new GuiItem(deleteItem, GuiGlobalActions.openGuiAction(createDeleteGui()), CustomAnvil.instance)); + + // Displayed item will be updated later + this.enchantSettingItem = new GuiItem(new ItemStack(Material.ENCHANTED_BOOK), event -> { + event.setCancelled(true); + EnchantSelectSettingGui enchantGui = new EnchantSelectSettingGui( + "§e" + CasedStringUtil.snakeToUpperSpacedCase(enchantConflict.toString()) + "§5", + this, this); + enchantGui.show(event.getWhoClicked()); + }, CustomAnvil.instance); + + this.groupSettingItem = new GuiItem(new ItemStack(Material.PAPER), event -> { + event.setCancelled(true); + GroupSelectSettingGui enchantGui = new GroupSelectSettingGui( + "§e" + CasedStringUtil.snakeToUpperSpacedCase(this.enchantConflict.toString()) + " §3Groups", + this, this, 0); + enchantGui.show(event.getWhoClicked()); + }, CustomAnvil.instance); + + this.minBeforeActiveSettingFactory = new IntSettingsGui.IntSettingFactory( + "§8Minimum enchantment count", + this, this.enchantConflict + ".maxEnchantmentBeforeConflict", ConfigHolder.CONFLICT_HOLDER, + Arrays.asList( + "§7Minimum enchantment count set to X mean only X enchantment can be put", + "§7on an item before the conflict is active." + ), + 0, 255, 0, 1 + ); + + this.pane.bindItem('E', this.enchantSettingItem); + this.pane.bindItem('G', this.groupSettingItem); + + // Now we update the items + updateLocal(); + + } + + private ConfirmActionGui createDeleteGui() { + Supplier deleteSupplier = () -> { + EnchantConflictManager manager = ConfigHolder.CONFLICT_HOLDER.getConflictManager(); + + // Remove from enchantment + manager.removeConflict(this.enchantConflict); + + // Remove from parent + this.parent.removeGeneric(this.enchantConflict); + + // Remove self + cleanAndBeUnusable(); + + // Update config file storage + ConfigHolder.CONFLICT_HOLDER.delete(this.enchantConflict.toString()); + + // Save + boolean success = true; + if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) { + success = ConfigHolder.CONFLICT_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + } + + return success; + }; + + return new ConfirmActionGui("§cDelete §e" + CasedStringUtil.snakeToUpperSpacedCase(this.enchantConflict.toString()) + "§c?", + "§7Confirm that you want to delete this conflict.", + this, this.parent, deleteSupplier + ); + } + + @Override + public void updateGuiValues() { + // update value from config to conflict + int minBeforeBlock = ConfigHolder.CONFLICT_HOLDER.getConfig().getInt(this.enchantConflict.toString()+'.'+EnchantConflictManager.ENCH_MAX_PATH, 0); + this.enchantConflict.setMinBeforeBlock(minBeforeBlock); + + // Parent should call updateLocal with this call + this.parent.updateValueForGeneric(this.enchantConflict, true); + } + + @Override + public void updateLocal() { + if (!this.shouldWork) return; + + // Prepare enchantment lore + ArrayList enchantLore = new ArrayList<>(); + enchantLore.add("§7Allow you to select a list of §5Enchantments §7that this conflict should include"); + Set enchants = getSelectedEnchantments(); + if (enchants.isEmpty()) { + enchantLore.add("§7There is no included enchantment for this conflict."); + } else { + enchantLore.add("§7List of included enchantment for this conflict:"); + Iterator enchantIterator = enchants.iterator(); + + boolean greaterThanMax = enchants.size() > 5; + int maxindex = (greaterThanMax ? 4 : enchants.size()); + for (int i = 0; i < maxindex; i++) { + // format string like "- Fire Protection" + String formattedName = CasedStringUtil.snakeToUpperSpacedCase(enchantIterator.next().getKey().getKey()); + enchantLore.add("§7- §5" + formattedName); + } + if (greaterThanMax) { + enchantLore.add("§7And " + (enchants.size() - 4) + " more..."); + } + + } + + // Prepare group lore + List groupLore = SelectGroupContainer.getGroupLore(this, "conflict", "exclude"); + + // Configure enchant setting item + ItemStack enchantItem = this.enchantSettingItem.getItem(); + ItemMeta enchantMeta = enchantItem.getItemMeta(); + assert enchantMeta != null; + + enchantMeta.setDisplayName("§aSelect included §5Enchantments §aSettings"); + enchantMeta.setLore(enchantLore); + + enchantItem.setItemMeta(enchantMeta); + + this.enchantSettingItem.setItem(enchantItem); // Just in case + + // Configure group setting item + ItemStack groupItem = this.groupSettingItem.getItem(); + ItemMeta groupMeta = groupItem.getItemMeta(); + assert groupMeta != null; + + groupMeta.setDisplayName("§aSelect Excluded §3Groups §aSettings"); + groupMeta.setLore(groupLore); + + groupItem.setItemMeta(groupMeta); + + this.groupSettingItem.setItem(groupItem); // Just in case + + this.pane.bindItem('M', this.minBeforeActiveSettingFactory.getItem(Material.COMMAND_BLOCK, + "Minimum Enchantment Count")); + update(); + } + + @Override + public void cleanAndBeUnusable() { + for (HumanEntity viewer : getViewers()) { + this.parent.show(viewer); + } + this.shouldWork = false; + + // Just in case something is extremely wrong + GuiItem background = GuiGlobalItems.backgroundItem(); + this.pane.bindItem('E', background); + this.pane.bindItem('G', background); + this.pane.bindItem('M', background); + this.pane.bindItem('D', background); + } + + @Override + public void show(@NotNull HumanEntity humanEntity) { + if (this.shouldWork) { + super.show(humanEntity); + } else { + this.parent.show(humanEntity); + } + } + + // Select enchantment container methods + + @Override + public Set getSelectedEnchantments() { + return this.enchantConflict.getEnchants(); + } + + @Override + public boolean setSelectedEnchantments(Set enchantments) { + if (!this.shouldWork) { + CustomAnvil.instance.getLogger().info("Trying to save " + enchantConflict + " enchants but sub config is destroyed"); + return false; + } + + // Set live configuration + this.enchantConflict.setEnchants(enchantments); + + // Save on file configuration + String[] enchantKeys = new String[enchantments.size()]; + int index = 0; + for (CAEnchantment enchantment : enchantments) { + enchantKeys[index++] = enchantment.getKey().toString(); + } + ConfigHolder.CONFLICT_HOLDER.getConfig().set(enchantConflict + ".enchantments", enchantKeys); + + try { + updateGuiValues(); + } catch (Exception e) { + CustomAnvil.instance.getLogger().log(Level.WARNING, "An error occurred while updating enchants for " + this.enchantConflict, e); + MetricsUtil.INSTANCE.trackError(e); + } + + // Save file configuration to disk + if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) { + return ConfigHolder.CONFLICT_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + } + + return true; + } + + @Override + public Set illegalEnchantments() { + return Collections.emptySet(); + } + + // Select group container methods + + @Override + public Set getSelectedGroups() { + return this.enchantConflict.getCantConflictGroup().getGroups(); + } + + @Override + public boolean setSelectedGroups(Set groups) { + if (!this.shouldWork) { + CustomAnvil.instance.getLogger().info("Trying to save " + enchantConflict.toString() + " groups but sub config is destroyed"); + return false; + } + + // Set live configuration + this.enchantConflict.getCantConflictGroup().setGroups(groups); + + // Save on file configuration + String[] groupsNames = new String[groups.size()]; + int index = 0; + for (AbstractMaterialGroup group : groups) { + groupsNames[index++] = group.getName(); + } + ConfigHolder.CONFLICT_HOLDER.getConfig().set(this.enchantConflict + ".notAffectedGroups", groupsNames); + + try { + updateGuiValues(); + } catch (Exception e) { + CustomAnvil.instance.getLogger().log(Level.WARNING, "An error occurred while updating group for " + this.enchantConflict, e); + MetricsUtil.INSTANCE.trackError(e); + } + + // Save file configuration to disk + if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) { + return ConfigHolder.CONFLICT_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + } + + return true; + } + + @Override + public Set illegalGroups() { + + return Collections.emptySet(); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/GroupConfigSubSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/GroupConfigSubSettingGui.java new file mode 100644 index 0000000..3d05674 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/GroupConfigSubSettingGui.java @@ -0,0 +1,412 @@ +package xyz.alexcrea.cuanvil.gui.config.list.elements; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.HumanEntity; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.group.*; +import xyz.alexcrea.cuanvil.gui.config.SelectGroupContainer; +import xyz.alexcrea.cuanvil.gui.config.SelectMaterialContainer; +import xyz.alexcrea.cuanvil.gui.config.ask.ConfirmActionGui; +import xyz.alexcrea.cuanvil.gui.config.global.GroupConfigGui; +import xyz.alexcrea.cuanvil.gui.config.settings.GroupSelectSettingGui; +import xyz.alexcrea.cuanvil.gui.config.settings.MaterialSelectSettingGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class GroupConfigSubSettingGui extends MappedToListSubSettingGui implements SelectGroupContainer, SelectMaterialContainer { + + private final GroupConfigGui parent; + private final IncludeGroup group; + private final PatternPane pane; + private boolean usable = true; + + public GroupConfigSubSettingGui( + @NotNull GroupConfigGui parent, + @NotNull IncludeGroup group) { + super(3, + "§e" + CasedStringUtil.snakeToUpperSpacedCase(group.getName()) + " §rConfig"); + this.parent = parent; + this.group = group; + + Pattern pattern = new Pattern( + GuiSharedConstant.EMPTY_GUI_FULL_LINE, + "00102000D", + "B00000000" + ); + this.pane = new PatternPane(0, 0, 9, 3, pattern); + addPane(this.pane); + + prepareStaticValues(); + } + + private GuiItem materialSelection; + private GuiItem groupSelection; + private void prepareStaticValues() { + GuiGlobalItems.addBackItem(this.pane, this.parent); + GuiGlobalItems.addBackgroundItem(this.pane); + + // Delete item + ItemStack deleteItem = new ItemStack(Material.RED_TERRACOTTA); + ItemMeta deleteMeta = deleteItem.getItemMeta(); + + deleteMeta.setDisplayName("§4DELETE GROUP"); + deleteMeta.setLore(Collections.singletonList("§cCaution with this button !")); + + deleteItem.setItemMeta(deleteMeta); + this.pane.bindItem('D', new GuiItem(deleteItem, openGuiAndCheckAction(), CustomAnvil.instance)); + + // Displayed item will be updated later + String materialSelectionName = "§e" + CasedStringUtil.snakeToUpperSpacedCase(group.getName()) + " §rMaterials"; + ItemStack selectItem = new ItemStack(Material.DIAMOND_SWORD); + ItemMeta selectItemMeta = selectItem.getItemMeta(); + selectItemMeta.setDisplayName(materialSelectionName); + + selectItem.setItemMeta(selectItemMeta); + this.materialSelection = new GuiItem(selectItem, (event) -> { + event.setCancelled(true); + MaterialSelectSettingGui selectGui = new MaterialSelectSettingGui(this, + materialSelectionName + , this); + selectGui.show(event.getWhoClicked()); + + }, CustomAnvil.instance); + + String selectGroupName = "§e" + CasedStringUtil.snakeToUpperSpacedCase(this.group.getName()) + " §rGroups"; + ItemStack selectGroup = new ItemStack(Material.CHEST); + ItemMeta selectGroupMeta = selectGroup.getItemMeta(); + selectGroupMeta.setDisplayName(selectGroupName); + + selectGroup.setItemMeta(selectGroupMeta); + this.groupSelection = new GuiItem(selectGroup, (event) -> { + event.setCancelled(true); + GroupSelectSettingGui enchantGui = new GroupSelectSettingGui( + selectGroupName, + this, this, 0); + enchantGui.show(event.getWhoClicked()); + }, CustomAnvil.instance); + + this.pane.bindItem('1', this.materialSelection); + this.pane.bindItem('2', this.groupSelection); + } + + private @NotNull Consumer openGuiAndCheckAction() { + ConfirmActionGui deleteGui = createDeleteGui(); + return event -> { + event.setCancelled(true); + HumanEntity player = event.getWhoClicked(); + // Do not allow to open inventory if player do not have edit configuration permission + if (!player.hasPermission(CustomAnvil.editConfigPermission)) { + player.closeInventory(); + player.sendMessage(GuiGlobalActions.NO_EDIT_PERM); + return; + } + // test if group is used & cancel & warn user if so + if(testAndWarnIfUsed(player)) return; + + deleteGui.show(player); + }; + } + + private @NotNull ConfirmActionGui createDeleteGui() { + Supplier deleteSupplier = () -> { + // test if group is used & cancel if so + if(!getUsedLocations(this.group).isEmpty()) return false; + + ItemGroupManager manager = ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager(); + + // Remove from manager + manager.getGroupMap().remove(this.group.getName()); + + // Remove from parent + this.parent.removeGeneric(this.group); + + // Remove self + cleanAndBeUnusable(); + + // Update config file storage + ConfigHolder.CUSTOM_RECIPE_HOLDER.delete(this.group.getName()); + + // Save + boolean success = true; + if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) { + success = ConfigHolder.CONFLICT_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + } + + return success; + }; + + return new ConfirmActionGui("§cDelete §e" + CasedStringUtil.snakeToUpperSpacedCase(this.group.toString()) + "§c?", + "§7Confirm that you want to delete this group.", + this, this.parent, deleteSupplier + ); + } + + public boolean testAndWarnIfUsed(HumanEntity player){ + List usedLoc = getUsedLocations(this.group); + if(usedLoc.isEmpty()){ + return false; + } + StringBuilder stb = new StringBuilder("§cCan't delete group " +this.group.getName()+ + "\n§eUsed by:"); + int maxIndex = usedLoc.size(); + int nbMore = 0; + if(maxIndex > 10){ + nbMore = maxIndex - 9; + maxIndex = 9; + } + for (int i = 0; i < maxIndex; i++) { + stb.append("\n§r-§e ").append(usedLoc.get(i)); + } + if(nbMore > 0){ + stb.append("§cAnd ").append(nbMore).append(" More..."); + } + + player.sendMessage(stb.toString()); + return true; + } + + // return a string containing every instance of where this group is used + public static List getUsedLocations(AbstractMaterialGroup group){ + ArrayList usageList = new ArrayList<>(); + + // Test used by another group + ItemGroupManager groupManager = ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager(); + for (AbstractMaterialGroup otherGroup : groupManager.getGroupMap().values()) { + if(otherGroup.getGroups().contains(group)) { + usageList.add("group " + otherGroup.getName()); + } + } + + // Test if used for conflict + EnchantConflictManager conflictManager = ConfigHolder.CONFLICT_HOLDER.getConflictManager(); + for (EnchantConflictGroup conflict : conflictManager.getConflictList()) { + if(conflict.getCantConflictGroup().getGroups().contains(group)) { + usageList.add("conflict " + conflict); + } + } + + return usageList; + } + + @Override + public void updateGuiValues() { + if(!this.usable) return; + // Parent should call updateLocal with this call + this.parent.updateValueForGeneric(this.group, true); + + } + + @Override + public void updateLocal() { + if(!this.usable) return; + // Prepare material lore + List matLore = SelectMaterialContainer.getMaterialLore(this, "group", "include"); + + // Prepare group lore + List groupLore = SelectGroupContainer.getGroupLore(this, "group", "include"); + + // Configure included material setting item + ItemStack matSelectItem = this.materialSelection.getItem(); + ItemMeta matSelectMeta = matSelectItem.getItemMeta(); + + matSelectMeta.setDisplayName("§aSelect included §eMaterials §aSettings"); + matSelectMeta.setLore(matLore); + matSelectMeta.addItemFlags(ItemFlag.values()); + + matSelectItem.setItemMeta(matSelectMeta); + + this.materialSelection.setItem(matSelectItem); // Just in case + + // Configure enchant setting item + ItemStack groupSelectItem = this.groupSelection.getItem(); + ItemMeta groupSelectMeta = groupSelectItem.getItemMeta(); + + groupSelectMeta.setDisplayName("§aSelect included §3Groups §aSettings"); + groupSelectMeta.setLore(groupLore); + + groupSelectItem.setItemMeta(groupSelectMeta); + + this.groupSelection.setItem(groupSelectItem); // Just in case + } + + @Override + public void cleanAndBeUnusable() { + this.usable = false; + this.pane.bindItem('1', GuiGlobalItems.backgroundItem()); + this.pane.bindItem('2', GuiGlobalItems.backgroundItem()); + this.pane.bindItem('D', GuiGlobalItems.backgroundItem()); + + } + + @Override + public void show(@NotNull HumanEntity player) { + if(!this.usable) { + this.parent.show(player); + return; + } + super.show(player); + } + + // ---------------------------- + // SelectGroupContainer related methods + // ---------------------------- + + @Override + public Set getSelectedGroups() { + return this.group.getGroups(); + } + + @Override + public boolean setSelectedGroups(Set groups) { + // update group and referencing groups + updateGroup(this.group, groups); + + // Save file configuration to disk + if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) { + return ConfigHolder.CONFLICT_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + } + + return true; + } + + private void updateGroup(@NotNull AbstractMaterialGroup group, Set groups){ + // Set live configuration + group.setGroups(groups); + + // Write to file configuration + groups = group.getGroups(); // Maybe some group may have been rejected + String[] groupNames = new String[groups.size()]; + int index = 0; + for (AbstractMaterialGroup otherGroup : groups) { + groupNames[index++] = otherGroup.getName(); + } + + ConfigHolder.ITEM_GROUP_HOLDER.getConfig().set(group.getName()+"."+ItemGroupManager.GROUP_LIST_PATH, groupNames); + + // Try to update referencing group. kind of expensive operation in some case. + updateDirectReferencingGroups(group); + + // We assume a backup & save call will be done soon after + } + + @Override + public Set illegalGroups() { + Set illegal = new HashSet<>(); + + for (AbstractMaterialGroup otherGroup : ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager().getGroupMap().values()) { + if(otherGroup.isReferencing(this.group)){ + illegal.add(otherGroup); + } + } + illegal.add(this.group); + + return illegal; + } + + // ---------------------------- + // End of SelectGroupContainer related methods + // ---------------------------- + // SelectMaterialContainer related methods + // ---------------------------- + + @Override + public Set getSelectedMaterials() { + return this.group.getNonGroupInheritedMaterials(); + } + + @Override + public boolean setSelectedMaterials(Set materials) { + this.group.setNonGroupInheritedMaterials(materials); + + // Write to file configuration + String[] groupNames = new String[materials.size()]; + int index = 0; + for (NamespacedKey otherGroup : materials) { + groupNames[index++] = otherGroup.getKey().toLowerCase(); + } + + ConfigHolder.ITEM_GROUP_HOLDER.getConfig().set(this.group.getName()+"."+ItemGroupManager.MATERIAL_LIST_PATH, groupNames); + + // update referencing groups + updateDirectReferencingGroups(this.group); + + // Save file configuration to disk + if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) { + return ConfigHolder.ITEM_GROUP_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + } + return true; + } + + @Override + public Set illegalMaterials() { + return Set.of(Material.AIR.getKey()); + } + + // ---------------------------- + // End of SelectMaterialContainer related methods + // ---------------------------- + + private void updateDirectReferencingGroups(AbstractMaterialGroup referenceTo){ + Collection everyStoredGroups = ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager().getGroupMap().values(); + List everyConflicts = ConfigHolder.CONFLICT_HOLDER.getConflictManager().getConflictList(); + + HashSet toUpdate = new HashSet<>(); + HashSet updateFuture = new HashSet<>(); + HashSet conflictGroupPlanned = new HashSet<>(); + + updateFuture.add(referenceTo); + while (!updateFuture.isEmpty()){ + HashSet temp = updateFuture; + updateFuture = toUpdate; + updateFuture.clear(); + toUpdate = temp; + + for (AbstractMaterialGroup testGroup : toUpdate) { + // Update other stored group + for (AbstractMaterialGroup otherGroup : everyStoredGroups) { + if(otherGroup.getGroups().contains(testGroup)){ + otherGroup.updateMaterials(); + updateFuture.add(otherGroup); + } + } + + // plan update for conflict groups + for (EnchantConflictGroup everyConflict : everyConflicts) { + AbstractMaterialGroup conflictGroup = everyConflict.getCantConflictGroup(); + if(conflictGroup.getGroups().contains(testGroup)){ + conflictGroupPlanned.add(conflictGroup); + } + } + + // Update parent & local by extension + if(testGroup instanceof IncludeGroup){ + this.parent.updateValueForGeneric((IncludeGroup) testGroup, false); + } + } + } + this.parent.update(); + + // Update conflict group + for (AbstractMaterialGroup conflictGroup : conflictGroupPlanned) { + conflictGroup.updateMaterials(); + } + + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/MappedToListSubSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/MappedToListSubSettingGui.java new file mode 100644 index 0000000..1d86781 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/MappedToListSubSettingGui.java @@ -0,0 +1,28 @@ +package xyz.alexcrea.cuanvil.gui.config.list.elements; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.ChestGui; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import io.delilaheve.CustomAnvil; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui; + +public abstract class MappedToListSubSettingGui extends ChestGui implements ValueUpdatableGui, ElementMappedToListGui { + + protected MappedToListSubSettingGui( + int rows, + @NotNull String title) { + super(rows, title, CustomAnvil.instance); + } + + @Override + public Gui getMappedGui() { + return this; + } + + @Override + public Gui getConnectedGui() { + return this; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/AbstractSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/AbstractSettingGui.java new file mode 100644 index 0000000..b07e7c1 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/AbstractSettingGui.java @@ -0,0 +1,136 @@ +package xyz.alexcrea.cuanvil.gui.config.settings; + +import com.github.stefvanschie.inventoryframework.adventuresupport.StringHolder; +import com.github.stefvanschie.inventoryframework.adventuresupport.TextHolder; +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.ChestGui; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; + +/** + * An instance gui used to edit a setting. + */ +public abstract class AbstractSettingGui extends ChestGui implements SettingGui { + + public static final String CLICK_LORE = "§7Click Here to change the value"; + + private PatternPane pane; + + /** + * Prepare necessary object for a setting gui. + * + * @param rows Number of row for this gui. + * @param title Title of this gui. + * @param parent Parent gui to go back when completed. + */ + protected AbstractSettingGui(int rows, @NotNull TextHolder title, ValueUpdatableGui parent) { + super(rows, title, CustomAnvil.instance); + initBase(parent); + } + + /** + * Prepare necessary object for a setting gui. + * + * @param rows Number of row for this gui. + * @param title Title of this gui. + * @param parent Parent gui to go back when completed. + */ + protected AbstractSettingGui(int rows, @NotNull String title, ValueUpdatableGui parent) { + this(rows, StringHolder.of(title), parent); + } + + protected GuiItem saveItem; + + /** + * Initialise and prepare value for this gui. + * + * @param parent Parent gui to go back when completed. + */ + protected void initBase(ValueUpdatableGui parent) { + Pattern pattern = getGuiPattern(); + pane = new PatternPane(0, 0, pattern.getLength(), pattern.getHeight(), pattern); + addPane(pane); + + GuiGlobalItems.addBackItem(pane, parent.getConnectedGui()); + GuiGlobalItems.addBackgroundItem(pane); + + saveItem = GuiGlobalItems.saveItem(this, parent); + + pane.bindItem('S', GuiGlobalItems.noChangeItem()); + + } + + @Override + public void update() { + pane.bindItem('S', hadChange() ? saveItem : GuiGlobalItems.noChangeItem()); + super.update(); + } + + /** + * Get main pane for this setting gui. + * + * @return Main pattern pain of this gui. + */ + protected PatternPane getPane() { + return pane; + } + + /** + * Used to get the gui pattern. + * Reserved character are: + *
      + *
    • S: save setting button.
    • + *
    • B: "back to previous gui" button.
    • + *
    • 0: default background item.
    • + *
    + * + * @return The gui's pattern. + */ + protected abstract Pattern getGuiPattern(); + + + /** + * Most of the time a setting gui will be called from a global gui. + *

    + * It is better to keep a factory that hold setting data than find what parameters to use every time. + */ + public abstract static class SettingGuiFactory implements SettingGui.SettingGuiFactory { + @NotNull + protected String configPath; + @NotNull + protected ConfigHolder config; + + /** + * Constructor for settings gui factory + * + * @param configPath Configuration path of this setting. + * @param config Configuration holder of this setting. + */ + protected SettingGuiFactory(@NotNull String configPath, @NotNull ConfigHolder config) { + this.configPath = configPath; + this.config = config; + } + + /** + * @return Configuration path of this setting. + */ + @NotNull + public String getConfigPath() { + return configPath; + } + + /** + * @return Configuration holder of this setting. + */ + @NotNull + public ConfigHolder getConfigHolder() { + return config; + } + + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/BoolSettingsGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/BoolSettingsGui.java new file mode 100644 index 0000000..1d5e73d --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/BoolSettingsGui.java @@ -0,0 +1,267 @@ +package xyz.alexcrea.cuanvil.gui.config.settings; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +/** + * An instance of a gui used to edit a boolean setting. + */ +public class BoolSettingsGui extends AbstractSettingGui { + + private final BoolSettingFactory holder; + private final boolean before; + private boolean now; + + /** + * Create a boolean setting config gui. + * + * @param holder Configuration factory of this setting. + * @param now The defined value of this setting. + */ + protected BoolSettingsGui(BoolSettingFactory holder, boolean now) { + super(3, holder.getTitle(), holder.parent); + this.holder = holder; + this.before = now; + this.now = now; + + prepareReturnToDefault(); + updateValueDisplay(); + } + + + @Override + public Pattern getGuiPattern() { + return new Pattern( + GuiSharedConstant.EMPTY_GUI_FULL_LINE, + "D0-0v0+00", + "B0000000S" + ); + } + + protected GuiItem returnToDefault; + + /** + * Prepare "return to default value" gui item. + */ + protected void prepareReturnToDefault() { + // Prepare default Value text + String defaultValueLore; + if(holder.defaultVal){ + defaultValueLore = "§aYes §7Is the default value"; + }else{ + defaultValueLore = "§cNo §7Is the default value"; + } + + // Create reset to default item + ItemStack item = new ItemStack(Material.COMMAND_BLOCK); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§eReset to default value"); + meta.setLore(Collections.singletonList(defaultValueLore)); + item.setItemMeta(meta); + returnToDefault = new GuiItem(item, event -> { + event.setCancelled(true); + now = holder.defaultVal; + updateValueDisplay(); + update(); + }, CustomAnvil.instance); + } + + /** + * Update item using the setting value to match the new value + */ + protected void updateValueDisplay() { + PatternPane pane = getPane(); + + // Get displayed value for this config. + String displayedName; + Material displayedMat; + if (now) { + displayedName = "§aYes"; + displayedMat = Material.GREEN_TERRACOTTA; + } else { + displayedName = "§cNo"; + displayedMat = Material.RED_TERRACOTTA; + } + + // create & set Value item + ArrayList valueLore = new ArrayList<>(); + if(!holder.displayLore.isEmpty()){ + valueLore.addAll(holder.displayLore); + valueLore.add(""); + } + valueLore.add(AbstractSettingGui.CLICK_LORE); + + ItemStack valueItemStack = new ItemStack(displayedMat); + ItemMeta valueMeta = valueItemStack.getItemMeta(); + assert valueMeta != null; + + valueMeta.setDisplayName(displayedName); + valueMeta.setLore(valueLore); + valueItemStack.setItemMeta(valueMeta); + GuiItem resultItem = new GuiItem(valueItemStack, inverseNowConsumer(), CustomAnvil.instance); + + pane.bindItem('v', resultItem); + + // reset to default + GuiItem returnToDefault; + if (now != holder.defaultVal) { + returnToDefault = this.returnToDefault; + } else { + returnToDefault = GuiGlobalItems.backgroundItem(); + } + pane.bindItem('D', returnToDefault); + + } + + /** + * @return A consumer to update the current setting's value. + */ + protected Consumer inverseNowConsumer() { + return event -> { + event.setCancelled(true); + now = !now; + updateValueDisplay(); + update(); + }; + } + + @Override + public boolean onSave() { + holder.config.getConfig().set(holder.configPath, now); + + if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) { + return holder.config.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + } + return true; + } + + @Override + public boolean hadChange() { + return now != before; + } + + /** + * A factory for a boolean setting gui that hold setting's information. + */ + public static class BoolSettingFactory extends SettingGuiFactory { + @NotNull + String title; + @NotNull + ValueUpdatableGui parent; + boolean defaultVal; + + @NotNull + List displayLore; + + /** + * Constructor for a boolean setting gui factory. + * + * @param title The title of the gui. + * @param parent Parent gui to go back when completed. + * @param config Configuration holder of this setting. + * @param configPath Configuration path of this setting. + * @param defaultVal Default value if not found on the config. + * @param displayLore Gui display item lore. + */ + public BoolSettingFactory( + @NotNull String title, @NotNull ValueUpdatableGui parent, + @NotNull ConfigHolder config, @NotNull String configPath, + boolean defaultVal, String... displayLore) { + super(configPath, config); + this.title = title; + this.parent = parent; + + this.defaultVal = defaultVal; + this.displayLore = Arrays.asList(displayLore); + } + + /** + * @return Get setting's gui title. + */ + @NotNull + public String getTitle() { + return title; + } + + /** + * @return The configured value for the associated setting. + */ + public boolean getConfiguredValue() { + return this.config.getConfig().getBoolean(this.configPath, this.defaultVal); + } + + @Override + public Gui create() { + // Get current value or default + boolean now = getConfiguredValue(); + // create new gui + return new BoolSettingsGui(this, now); + } + + /** + * Create a new Boolean setting GuiItem. + * This item will create and open a boolean setting GUI from the factory. + * The item will have its value written in the lore part of the item. + * + * @param name Name of the item. + * @return A formatted GuiItem that will create and open a GUI for the boolean setting. + */ + public GuiItem getItem(String name){ + // Get item properties + boolean value = getConfiguredValue(); + + Material itemMat; + StringBuilder itemName = new StringBuilder("§e"); + String finalValue; + if (value) { + itemMat = Material.GREEN_TERRACOTTA; + finalValue = "§aYes"; + } else { + itemMat = Material.RED_TERRACOTTA; + finalValue = "§cNo"; + } + itemName.append(name); + + return GuiGlobalItems.createGuiItemFromProperties(this, itemMat, itemName, finalValue, this.displayLore, false); + } + + /** + * Create a new boolean setting GuiItem. + * This item will create and open a boolean setting GUI from the factory. + * The item will have its value written in the lore part of the item. + * Item's name will be the factory set title. + * + * @return A formatted GuiItem that will create and open a GUI for the boolean setting. + */ + public GuiItem getItem(){ + // Get item properties + String configPath = GuiGlobalItems.getConfigNameFromPath(getConfigPath()); + + return getItem(CasedStringUtil.detectToUpperSpacedCase(configPath)); + } + + } + +} + diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/DoubleSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/DoubleSettingGui.java new file mode 100644 index 0000000..c0dcff6 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/DoubleSettingGui.java @@ -0,0 +1,470 @@ +package xyz.alexcrea.cuanvil.gui.config.settings; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +public class DoubleSettingGui extends AbstractSettingGui { + + protected final DoubleSettingFactory holder; + protected final boolean asPercentage; + protected final boolean nullOnZero; + @NotNull + protected final BigDecimal before; + @NotNull + protected BigDecimal now; + protected BigDecimal step; + + /** + * Create a double setting config gui. + * + * @param holder Configuration factory of this setting. + * @param now The defined value of this setting. + * @param asPercentage If the value represent a % + * @param nullOnZero If the value should be set as null on zero + */ + protected DoubleSettingGui(DoubleSettingFactory holder, @NotNull BigDecimal now, + boolean asPercentage, boolean nullOnZero) { + super(3, holder.getTitle(), holder.parent); + assert holder.steps.length > 0 && holder.steps.length <= 9; + this.holder = holder; + this.asPercentage = asPercentage; + this.nullOnZero = nullOnZero; + + this.before = now; + this.now = now; + this.step = holder.steps[0]; + + initStepsValue(); + prepareReturnToDefault(); + updateValueDisplay(); + } + + private static final ItemStack DELETE_ITEM_STACK = new ItemStack(Material.RED_TERRACOTTA); + static { + ItemMeta meta = DELETE_ITEM_STACK.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§cDisable item being repaired ?"); + meta.setLore(Arrays.asList("§7Confirm disabling unit repair for this item..", + "§4Cation: This action can't be canceled.")); + + DELETE_ITEM_STACK.setItemMeta(meta); + } + + private GuiItem askDelete; + + @Override + protected void initBase(ValueUpdatableGui parent) { + super.initBase(parent); + + this.askDelete = new GuiItem(DELETE_ITEM_STACK, + GuiGlobalActions.saveSettingAction(this, parent), + CustomAnvil.instance); + } + + @Override + public void update() { + boolean shouldDelete = isNull() && hadChange(); + + GuiItem tempSaveItem = this.saveItem; + if(shouldDelete){ + this.saveItem = this.askDelete; + } + + super.update(); + + if(shouldDelete){ + this.saveItem = tempSaveItem; + } + } + + @Override + public Pattern getGuiPattern() { + return new Pattern( + "abcdefghi", + "D0-0v0+00", + "B0000000S" + ); + } + + protected GuiItem returnToDefault; + + /** + * Prepare "return to default value" gui item. + */ + protected void prepareReturnToDefault() { + ItemStack item = new ItemStack(Material.COMMAND_BLOCK); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§eReset to default value"); + meta.setLore(Collections.singletonList("§7Default value is §e" + displayValue(holder.defaultVal))); + item.setItemMeta(meta); + returnToDefault = new GuiItem(item, event -> { + event.setCancelled(true); + now = holder.defaultVal; + updateValueDisplay(); + update(); + }, CustomAnvil.instance); + } + + /** + * Update item using the setting value to match the new value. + */ + protected void updateValueDisplay() { + + PatternPane pane = getPane(); + + //minus item + GuiItem minusItem; + if (now.compareTo(holder.min) > 0) { + BigDecimal planned = holder.min.max(now.subtract(step)); + + minusItem = getSetValueItem(Material.RED_TERRACOTTA, planned, "§c-"); + } else { + minusItem = GuiGlobalItems.backgroundItem(Material.BARRIER); + } + pane.bindItem('-', minusItem); + + //plus item + GuiItem plusItem; + if (now.compareTo(holder.max) < 0) { + BigDecimal planned = holder.max.min(now.add(step)); + + plusItem = getSetValueItem(Material.GREEN_TERRACOTTA, planned, "§a+"); + } else { + plusItem = GuiGlobalItems.backgroundItem(Material.BARRIER); + } + pane.bindItem('+', plusItem); + + // "result" display + ItemStack resultPaper = new ItemStack(Material.PAPER); + ItemMeta resultMeta = resultPaper.getItemMeta(); + assert resultMeta != null; + + resultMeta.setDisplayName("§fValue: §e" + displayValue(now)); + resultPaper.setItemMeta(resultMeta); + GuiItem resultItem = new GuiItem(resultPaper, GuiGlobalActions.stayInPlace, CustomAnvil.instance); + + pane.bindItem('v', resultItem); + + // reset to default + GuiItem returnToDefault; + if (now.compareTo(holder.defaultVal) != 0) { + returnToDefault = this.returnToDefault; + } else { + returnToDefault = GuiGlobalItems.backgroundItem(); + } + pane.bindItem('D', returnToDefault); + + } + + private GuiItem getSetValueItem(Material mat, BigDecimal planned, String numberPrefix){ + // Create set item lore + ArrayList setLoreItem = new ArrayList<>(); + if(!holder.displayLore.isEmpty()){ + setLoreItem.addAll(holder.displayLore); + setLoreItem.add(""); + } + setLoreItem.add(AbstractSettingGui.CLICK_LORE); + + // Create & return set value item + ItemStack item = new ItemStack(mat); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§e" + displayValue(now) + " §f-> §e" + displayValue(planned) + + " §r(" + numberPrefix + (displayValue(planned.subtract(now).abs()) + "§r)")); + meta.setLore(setLoreItem); + item.setItemMeta(meta); + + return new GuiItem(item, updateNowConsumer(planned), CustomAnvil.instance); + } + + /** + * @param planned Value to change current setting to. + * @return A consumer to update the current setting's value. + */ + protected Consumer updateNowConsumer(BigDecimal planned) { + return event -> { + event.setCancelled(true); + now = planned; + updateValueDisplay(); + update(); + }; + } + + /** + * Initialise step items. + */ + protected void initStepsValue() { + // Put background glass on the background of 'a' to 'b' + GuiItem background = GuiGlobalItems.backgroundItem(); + PatternPane pane = getPane(); + + for (char i = 'a'; i < (getMidStepChar() - 'a') * 2 + 1; i++) { + pane.bindItem(i, background); + } + // Then update legit step values + updateStepValue(); + } + + /** + * Update steps items value. + */ + protected void updateStepValue() { + if (holder.steps.length <= 1) return; + // We assume steps have a length of 2k+1 cause its more pretty + char val = getMidStepChar(); + // Offset to start (not the best way to do it) + val -= (char) ((holder.steps.length - 1) / 2); + + // Then place items + PatternPane pane = getPane(); + for (int i = 0; i < holder.steps.length; i++) { + pane.bindItem(val + i, stepGuiItem(i)); + } + + } + + /** + * Step use lower case character from 'a' to a certain char. + * + * @return The middle value of the character set for steps. + */ + protected char getMidStepChar() { + return 'e'; + } + + /** + * Create a step item from a step value. + * + * @param stepIndex the index of the step item. + * @return A step item corresponding to its index value. + */ + protected GuiItem stepGuiItem(int stepIndex) { + BigDecimal stepValue = holder.steps[stepIndex]; + + // Get material properties + Material stepMat; + StringBuilder stepName = new StringBuilder("§"); + List stepLore; + Consumer clickEvent; + if (stepValue.compareTo(step) == 0) { + stepMat = Material.GREEN_STAINED_GLASS_PANE; + stepName.append('a'); + stepLore = Collections.singletonList("§7Value is changing by " + displayValue(stepValue)); + clickEvent = GuiGlobalActions.stayInPlace; + } else { + stepMat = Material.RED_STAINED_GLASS_PANE; + stepName.append('c'); + stepLore = Collections.singletonList("§7Click here to change the value by " + displayValue(stepValue)); + clickEvent = updateStepValue(stepValue); + } + stepName.append("Step of §e").append(displayValue(stepValue)); + + // Create item stack then gui item + ItemStack item = new ItemStack(stepMat); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName(stepName.toString()); + meta.setLore(stepLore); + item.setItemMeta(meta); + + return new GuiItem(item, clickEvent, CustomAnvil.instance); + } + + /** + * @param stepValue Value to change current step to. + * @return A consumer to update the current step of this setting. + */ + protected Consumer updateStepValue(BigDecimal stepValue) { + return event -> { + event.setCancelled(true); + this.step = stepValue; + updateStepValue(); + updateValueDisplay(); + update(); + }; + } + + @Override + public boolean onSave() { + if(isNull()){ + if(this.holder.config instanceof ConfigHolder.DeletableResource deletableResource){ + deletableResource.delete(this.holder.configPath); + }else{ + this.holder.config.getConfig().set(this.holder.configPath, null); + } + }else{ + this.holder.config.getConfig().set(this.holder.configPath, now.doubleValue()); + } + + if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) { + return holder.config.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + } + return true; + } + + @Override + public boolean hadChange() { + return now.compareTo(before) != 0; + } + + public boolean isNull(){ + return this.nullOnZero && (this.now.compareTo(BigDecimal.ZERO) == 0); + } + + private static final BigDecimal PERCENTAGE_OFFSET = BigDecimal.valueOf(100); + public String displayValue(BigDecimal value){ + return displayValue(value, this.asPercentage); + } + + public static String displayValue(BigDecimal value, boolean isAsPercentage){ + if(isAsPercentage){ + return value.multiply(PERCENTAGE_OFFSET).setScale(value.scale()-2, RoundingMode.HALF_UP) + "%"; + } + return value.toString(); + } + + /** + * A factory for a double setting gui that hold setting's information. + */ + public static class DoubleSettingFactory extends SettingGuiFactory { + @NotNull + String title; + @NotNull + ValueUpdatableGui parent; + + int scale; + boolean asPercentage; + boolean nullOnZero; + BigDecimal min; + BigDecimal max; + BigDecimal defaultVal; + BigDecimal[] steps; + + @NotNull + List displayLore; + + /** + * Constructor for a double setting gui factory. + * + * @param title The title of the gui. + * @param parent Parent gui to go back when completed. + * @param config Configuration holder of this setting. + * @param configPath Configuration path of this setting. + * @param displayLore Gui display item lore. + * @param scale The scale of the decimal. + * @param asPercentage If we should display the value as a %. + * @param nullOnZero Set the value as null (deleting it) when equal to 0 + * @param min Minimum value of this setting. + * @param max Maximum value of this setting. + * @param defaultVal Default value if not found on the config. + * @param steps List of step the value can increment/decrement. + * List's size should be between 1 (included) and 5 (included). + * it is visually preferable to have an odd number of step. + * If step only contain 1 value, no step item should be displayed. + */ + public DoubleSettingFactory( + @NotNull String title, @NotNull ValueUpdatableGui parent, + @NotNull ConfigHolder config, + @NotNull String configPath, + @Nullable List displayLore, + int scale, boolean asPercentage, boolean nullOnZero, + double min, double max, double defaultVal, double... steps) { + super(configPath, config); + this.title = title; + this.parent = parent; + this.scale = scale; + this.asPercentage = asPercentage; + this.nullOnZero = nullOnZero; + this.min = BigDecimal.valueOf(min).setScale(scale, RoundingMode.HALF_UP); + this.max = BigDecimal.valueOf(max).setScale(scale, RoundingMode.HALF_UP); + this.defaultVal = BigDecimal.valueOf(defaultVal).setScale(scale, RoundingMode.HALF_UP); + + this.steps = new BigDecimal[steps.length]; + for (int i = 0; i < steps.length; i++) { + this.steps[i] = BigDecimal.valueOf(steps[i]).setScale(scale, RoundingMode.HALF_UP); + } + + if(displayLore == null){ + this.displayLore = Collections.emptyList(); + }else { + this.displayLore = displayLore; + } + } + + /** + * @return Get setting's gui title + */ + @NotNull + public String getTitle() { + return title; + } + + /** + * @return The configured value for the associated setting. + */ + public BigDecimal getConfiguredValue() { + ConfigurationSection section = this.config.getConfig(); + if(section.isDouble(this.configPath)){ + return BigDecimal.valueOf(section.getDouble(this.configPath)).setScale(2, RoundingMode.HALF_UP); + } + return this.defaultVal; + } + + @Override + public Gui create() { + // Get value or default + BigDecimal now = getConfiguredValue(); + // create new gui + return new DoubleSettingGui(this, now, this.asPercentage, this.nullOnZero); + } + + + public GuiItem getItem(Material itemMat, String name){ + // Get item properties + BigDecimal value = getConfiguredValue(); + StringBuilder itemName = new StringBuilder("§a").append(name); + + return GuiGlobalItems.createGuiItemFromProperties(this, itemMat, itemName, + "§e" + displayValue(value, this.asPercentage), + this.displayLore, true); + } + + public GuiItem getItem(Material itemMat){ + // Get item properties + String configPath = GuiGlobalItems.getConfigNameFromPath(getConfigPath()); + + return getItem(itemMat, CasedStringUtil.detectToUpperSpacedCase(configPath)); + } + + } + +} \ No newline at end of file diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/EnchantCostSettingsGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/EnchantCostSettingsGui.java new file mode 100644 index 0000000..3bd923b --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/EnchantCostSettingsGui.java @@ -0,0 +1,311 @@ +package xyz.alexcrea.cuanvil.gui.config.settings; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import io.delilaheve.util.ConfigOptions; +import org.bukkit.Material; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +/** + * An instance of a gui used to edit an enchantment cost setting. + * May be considered as a 2 int setting. + */ +public class EnchantCostSettingsGui extends IntSettingsGui { + + protected final static String ITEM_PATH = ".item"; + protected final static String BOOK_PATH = ".book"; + + private int beforeBook; + private int nowBook; + + /** + * Create an enchantment cost setting config gui. + * + * @param holder Configuration factory of this setting. + * @param nowItem The defined value of this setting item's value. + */ + protected EnchantCostSettingsGui(EnchantCostSettingFactory holder, int nowItem) { + super(holder, nowItem); + + this.step = holder.steps[0]; + + initStaticItem(); + } + + /** + * Initialise step items. + * Also bypassed init sequence to initialise books value + * as it used as one of the first call from the init sequence of the gui + */ + @Override + protected void initStepsValue() { + super.initStepsValue(); + + int nowBook = ((EnchantCostSettingFactory) this.holder).getConfiguredBookValue(); + this.beforeBook = nowBook; + this.nowBook = nowBook; + } + + @Override + public Pattern getGuiPattern() { + return new Pattern( + "abc13-v+0", + "D0012MVP0", + "B0010000S" + ); + } + + /** + * Initialise item that should not be updated late. + */ + private void initStaticItem() { + PatternPane pane = getPane(); + + // book display + ItemStack bookItemstack = new ItemStack(Material.BOOK); + ItemMeta bookMeta = bookItemstack.getItemMeta(); + assert bookMeta != null; + + bookMeta.setDisplayName("§aCost of an Enchantment by Book"); + bookMeta.setLore(Arrays.asList( + "§7Cost per result item level of an sacrifice enchantment", + "§7Only apply if sacrificed item §cis §7a book")); + bookItemstack.setItemMeta(bookMeta); + + // sword display + ItemStack swordItemstack = new ItemStack(Material.WOODEN_SWORD); + ItemMeta swordMeta = swordItemstack.getItemMeta(); + assert swordMeta != null; + + swordMeta.addItemFlags(ItemFlag.values()); + swordMeta.setDisplayName("§aCost of an Enchantment by Item"); + swordMeta.setLore(Arrays.asList( + "§7Cost per result item level of an sacrifice enchantment", + "§7Only apply if sacrificed item §cis not §7a book")); + swordItemstack.setItemMeta(swordMeta); + + pane.bindItem('1', GuiGlobalItems.backgroundItem(Material.BLACK_STAINED_GLASS_PANE)); + pane.bindItem('2', new GuiItem(bookItemstack, GuiGlobalActions.stayInPlace, CustomAnvil.instance)); + pane.bindItem('3', new GuiItem(swordItemstack, GuiGlobalActions.stayInPlace, CustomAnvil.instance)); + } + + @Override + protected void prepareReturnToDefault() { + ItemStack item = new ItemStack(Material.COMMAND_BLOCK); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + // assume holder is an instance of EnchantCostSettingFactory + EnchantCostSettingFactory holder = (EnchantCostSettingFactory) this.holder; + + meta.setDisplayName("§eReset to default value"); + meta.setLore(Arrays.asList( + "§7Default item value is: §e" + holder.defaultVal, + "§7Default book value is: §e" + holder.defaultBookVal)); + item.setItemMeta(meta); + returnToDefault = new GuiItem(item, event -> { + event.setCancelled(true); + nowBook = holder.defaultBookVal; + now = holder.defaultVal; + updateValueDisplay(); + update(); + }, CustomAnvil.instance); + } + + @Override + protected void updateValueDisplay() { + super.updateValueDisplay(); + PatternPane pane = getPane(); + + // assume holder is an instance of EnchantCostSettingFactory + EnchantCostSettingFactory holder = ((EnchantCostSettingFactory) this.holder); + + int nowBook = this.nowBook; + + // minus item + GuiItem minusItem; + if (nowBook > holder.min) { + int planned = Math.max(holder.min, nowBook - step); + ItemStack item = new ItemStack(Material.RED_TERRACOTTA); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§e" + nowBook + " §f-> §e" + planned + " §r(§c-" + (nowBook - planned) + "§r)"); + meta.setLore(Collections.singletonList(AbstractSettingGui.CLICK_LORE)); + item.setItemMeta(meta); + + minusItem = new GuiItem(item, updateNowBookConsumer(planned), CustomAnvil.instance); + } else { + minusItem = GuiGlobalItems.backgroundItem(Material.BARRIER); + } + pane.bindItem('M', minusItem); + + //plus item + GuiItem plusItem; + if (nowBook < holder.max) { + int planned = Math.min(holder.max, nowBook + step); + ItemStack item = new ItemStack(Material.GREEN_TERRACOTTA); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§e" + nowBook + " §f-> §e" + planned + " §r(§a+" + (planned - nowBook) + "§r)"); + meta.setLore(Collections.singletonList(AbstractSettingGui.CLICK_LORE)); + item.setItemMeta(meta); + + plusItem = new GuiItem(item, updateNowBookConsumer(planned), CustomAnvil.instance); + } else { + plusItem = GuiGlobalItems.backgroundItem(Material.BARRIER); + } + pane.bindItem('P', plusItem); + + // now value display + ItemStack nowPaper = new ItemStack(Material.PAPER); + ItemMeta nowMeta = nowPaper.getItemMeta(); + assert nowMeta != null; + + nowMeta.setDisplayName("§fValue: §e" + nowBook); + if(!holder.displayLore.isEmpty()){ + nowMeta.setLore(holder.displayLore); + } + + nowPaper.setItemMeta(nowMeta); + + GuiItem resultItem = new GuiItem(nowPaper, GuiGlobalActions.stayInPlace, CustomAnvil.instance); + + pane.bindItem('V', resultItem); + + // reset to default + GuiItem returnToDefault; + if (now != holder.defaultVal || nowBook != holder.defaultBookVal) { + returnToDefault = this.returnToDefault; + } else { + returnToDefault = GuiGlobalItems.backgroundItem(); + } + pane.bindItem('D', returnToDefault); + + + } + + /** + * @param planned Value to change current book cost setting to. + * @return A consumer to update the current book cost setting's value. + */ + protected Consumer updateNowBookConsumer(int planned) { + return event -> { + event.setCancelled(true); + nowBook = planned; + updateValueDisplay(); + update(); + }; + } + + @Override + protected char getMidStepChar() { + return 'b'; + } + + @Override + public boolean onSave() { + holder.config.getConfig().set(holder.configPath + ITEM_PATH, now); + holder.config.getConfig().set(holder.configPath + BOOK_PATH, nowBook); + + if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) { + return holder.config.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + } + return true; + } + + @Override + public boolean hadChange() { + return super.hadChange() || nowBook != beforeBook; + } + + /** + * A factory for an enchantment cost setting gui that hold setting's information. + */ + public static class EnchantCostSettingFactory extends IntSettingsGui.IntSettingFactory { + + int defaultBookVal; + @NotNull CAEnchantment enchantment; + + /** + * Constructor for an enchantment cost setting gui factory. + * + * @param title The title of the gui. + * @param parent Parent gui to go back when completed. + * @param configPath Configuration path of this setting. + * @param config Configuration holder of this setting. + * @param displayLore Gui display item lore. + * @param min Minimum value of this setting. + * @param max Maximum value of this setting. + * @param enchantment Enchantment to change the cost to + * @param steps List of step the value can increment/decrement. + * List's size should be between 1 (included) and 3 (included). + * it is visually preferable to have an odd number of step. + * If step only contain 1 value, no step item should be displayed. + */ + public EnchantCostSettingFactory( + @NotNull String title, ValueUpdatableGui parent, + @NotNull String configPath, @NotNull ConfigHolder config, + @Nullable List displayLore, + @NotNull CAEnchantment enchantment, + int min, int max, int... steps) { + + super(title, parent, + configPath, config, + displayLore, + min, max, enchantment.defaultRarity().getItemValue(), + steps); + + this.defaultBookVal = enchantment.defaultRarity().getBookValue(); + this.enchantment = enchantment; + } + + /** + * @return The configured value for the enchant setting item value. + */ + @Override + public int getConfiguredValue() { + return ConfigOptions.INSTANCE.enchantmentValue(enchantment, false); + } + + /** + * @return The configured value for the enchant setting book value. + */ + public int getConfiguredBookValue() { + return ConfigOptions.INSTANCE.enchantmentValue(enchantment, true); + } + + @Override + public Gui create() { + // Get value or default + int nowItem = getConfiguredValue(); + // create new gui + return new EnchantCostSettingsGui(this, nowItem); + } + + public List getDisplayLore() { + return this.displayLore; + } + } + +} \ No newline at end of file diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/EnchantSelectSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/EnchantSelectSettingGui.java new file mode 100644 index 0000000..176da55 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/EnchantSelectSettingGui.java @@ -0,0 +1,217 @@ +package xyz.alexcrea.cuanvil.gui.config.settings; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry; +import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui; +import xyz.alexcrea.cuanvil.gui.config.MainConfigGui; +import xyz.alexcrea.cuanvil.gui.config.SelectEnchantmentContainer; +import xyz.alexcrea.cuanvil.gui.config.list.SettingGuiListConfigGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Stream; + +public class EnchantSelectSettingGui extends SettingGuiListConfigGui implements SettingGui { + + private final SelectEnchantmentContainer enchantContainer; + + private final Set selectedEnchant; + private final GuiItem saveItem; + + private boolean displayUnselected; + + public EnchantSelectSettingGui(@NotNull String title, ValueUpdatableGui parent, SelectEnchantmentContainer enchantContainer) { + super(title, parent instanceof Gui parentGui ? parentGui : MainConfigGui.getInstance()) ; + this.enchantContainer = enchantContainer; + + this.selectedEnchant = new HashSet<>(enchantContainer.getSelectedEnchantments()); + + this.saveItem = GuiGlobalItems.saveItem(this, parent); + this.backgroundPane.bindItem('S', GuiGlobalItems.noChangeItem()); + + this.displayUnselected = true; + this.backgroundPane.bindItem('b', createDisplayUnusedItem()); + + init(); + } + + @Override + protected Pattern getBackgroundPattern() { + return new Pattern( + GuiSharedConstant.UPPER_FILLER_FULL_PLANE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + "B11LbR11S" + ); + } + + @Override + protected Collection getEveryDisplayableInstanceOfGeneric() { + Stream toDisplayStream; + if(this.displayUnselected){ + toDisplayStream = CAEnchantmentRegistry.getInstance().getNameSortedEnchantments().stream(); + }else{ + toDisplayStream = this.selectedEnchant.stream().sorted(Comparator.comparing(CAEnchantment::getName)); + } + Set illegalEnchantments = this.enchantContainer.illegalEnchantments(); + + + return toDisplayStream + .filter(enchantment -> !illegalEnchantments.contains(enchantment)) + .toList(); + } + + @Override + public void update() { + this.backgroundPane.bindItem('S', hadChange() ? saveItem : GuiGlobalItems.noChangeItem()); + super.update(); + } + + @Override + protected GuiItem itemFromFactory(CAEnchantment enchantment, DummyFactory factory) { + boolean isIn = this.selectedEnchant.contains(enchantment); + + Material usedMaterial; + if (isIn) { + usedMaterial = Material.ENCHANTED_BOOK; + } else { + usedMaterial = Material.BOOK; + } + ItemStack item = new ItemStack(usedMaterial); + + setEnchantItemMeta(item, enchantment.getKey().getKey(), isIn); + + GuiItem guiItem = new GuiItem(item, CustomAnvil.instance); + guiItem.setAction(getEnchantItemConsumer(enchantment, guiItem)); + return guiItem; + } + + private GuiItem createDisplayUnusedItem() { + ItemStack item = new ItemStack(this.displayUnselected ? Material.BOOK : Material.ENCHANTED_BOOK); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName((this.displayUnselected ? "§aEverything displayed" : "§eOnly selected displayed")); + meta.setLore(Collections.singletonList( + "§7Click here to see " + + (this.displayUnselected ? "only selected" : "every") + + " enchantments")); + + item.setItemMeta(meta); + + return new GuiItem(item, clickEvent -> { + clickEvent.setCancelled(true); + this.displayUnselected = !this.displayUnselected; + + this.backgroundPane.bindItem('b', createDisplayUnusedItem()); + reloadValues(); + }, CustomAnvil.instance); + } + + private static final List TRUE_LORE = Collections.singletonList("§7Value: §aSelected"); + private static final List FALSE_LORE = Collections.singletonList("§7Value: §cNot Selected"); + + public void setEnchantItemMeta(ItemStack item, String name, boolean isIn) { + ItemMeta meta = item.getItemMeta(); + + if (meta == null) { + CustomAnvil.instance.getLogger().warning("Could not create item for enchantment: " + name + ":\n" + + "Item do not gave item meta: " + item + ". Using a placeholder item instead"); + item.setType(Material.PAPER); + meta = item.getItemMeta(); + assert meta != null; + } + + meta.setDisplayName("§" + (isIn ? 'a' : 'c') + CasedStringUtil.snakeToUpperSpacedCase(name)); + if (isIn) { + meta.addEnchant(Enchantment.DAMAGE_UNDEAD, 1, true); + meta.setLore(TRUE_LORE); + } else { + meta.removeEnchant(Enchantment.DAMAGE_UNDEAD); + meta.setLore(FALSE_LORE); + } + meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_ENCHANTS); + + item.setItemMeta(meta); + } + + private Consumer getEnchantItemConsumer(CAEnchantment enchant, GuiItem guiItem) { + return event -> { + event.setCancelled(true); + + ItemStack item = guiItem.getItem(); + + boolean isIn = this.selectedEnchant.contains(enchant); + if (isIn) { + this.selectedEnchant.remove(enchant); + item.setType(Material.BOOK); + } else { + this.selectedEnchant.add(enchant); + item.setType(Material.ENCHANTED_BOOK); + } + + setEnchantItemMeta(item, enchant.getKey().getKey(), !isIn); + guiItem.setItem(item);// Just in case + + update(); + }; + } + + @Override + public boolean onSave() { + return this.enchantContainer.setSelectedEnchantments(this.selectedEnchant); + } + + @Override + public boolean hadChange() { + Set baseGroup = this.enchantContainer.getSelectedEnchantments(); + return baseGroup.size() != this.selectedEnchant.size() || + !baseGroup.containsAll(this.selectedEnchant); + } + + + // Unused methods and class + public static class DummyFactory extends AbstractSettingGui.SettingGuiFactory{ + protected DummyFactory(@NotNull String configPath, @NotNull ConfigHolder config) { + super(configPath, config); + } + @Override + public Gui create() { + return null; + } + } + @Override + protected List getCreateItemLore() { + return Collections.emptyList(); + } + @Override + protected Consumer getCreateClickConsumer() { + return null; + } + @Override + protected String createItemName() { + return null; + } + @Override + protected DummyFactory createFactory(CAEnchantment generic) { + return null; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/EnumSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/EnumSettingGui.java new file mode 100644 index 0000000..5bf4c24 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/EnumSettingGui.java @@ -0,0 +1,242 @@ +package xyz.alexcrea.cuanvil.gui.config.settings; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; + +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +public class EnumSettingGui & EnumSettingGui.ConfigurableEnum> extends AbstractSettingGui { + + private final EnumSettingFactory holder; + private final T before; + private T now; + + /** + * Create an item setting config gui. + * + * @param holder Configuration factory of this setting. + * @param now The defined value of this setting. + */ + protected EnumSettingGui(EnumSettingFactory holder, T now) { + super(3, holder.getTitle(), holder.parent); + this.holder = holder; + this.before = now; + this.now = now; + + prepareStaticItems(); + updateValueDisplay(); + } + + @Override + public Pattern getGuiPattern() { + return new Pattern( + GuiSharedConstant.EMPTY_GUI_FULL_LINE, + "D000v0000", + "B0000000S" + ); + } + + + public void prepareStaticItems(){ + prepareReturnToDefault(); + } + + + protected GuiItem returnToDefault; + + /** + * Prepare "return to default value" gui item. + */ + protected void prepareReturnToDefault() { + ItemStack item = new ItemStack(Material.COMMAND_BLOCK); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§eReset to default value"); + meta.setLore(Collections.singletonList("§7Default value is §e" + holder.getDefault().configurationGuiName())); + item.setItemMeta(meta); + returnToDefault = new GuiItem(item, event -> { + event.setCancelled(true); + now = holder.getDefault(); + updateValueDisplay(); + update(); + }, CustomAnvil.instance); + } + + /** + * Update item using the setting value to match the new value + */ + protected void updateValueDisplay() { + PatternPane pane = getPane(); + + // Get displayed value for this config. + ItemStack displayedItem = now.configurationGuiItem(); + + GuiItem resultItem = new GuiItem(displayedItem, selectNext(), CustomAnvil.instance); + pane.bindItem('v', resultItem); + + // reset to default + GuiItem returnToDefault; + if (now != holder.getDefault()) { + returnToDefault = this.returnToDefault; + } else { + returnToDefault = GuiGlobalItems.backgroundItem(); + } + pane.bindItem('D', returnToDefault); + + } + + /** + * @return A consumer to update the current setting's value. + */ + protected Consumer selectNext() { + return event -> { + event.setCancelled(true); + + this.now = this.holder.next(this.now); + + updateValueDisplay(); + update(); + }; + + } + + @Override + public boolean onSave() { + holder.config.getConfig().set(holder.configPath, this.now.configName()); + + if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) { + return holder.config.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + } + return true; + } + + @Override + public boolean hadChange() { + return !now.equals(before); + } + + + /** + * A factory for an enum setting gui that hold setting's information. + */ + public abstract static class EnumSettingFactory & ConfigurableEnum> extends SettingGuiFactory { + @NotNull + String title; + @NotNull + ValueUpdatableGui parent; + + /** + * Constructor for an enum settings gui factory + * + * @param title The title of the gui. + * @param parent Parent gui to go back when completed. + * @param configPath Configuration path of this setting. + * @param config Configuration holder of this setting. + */ + protected EnumSettingFactory( + @NotNull String title, @NotNull ValueUpdatableGui parent, + @NotNull String configPath, @NotNull ConfigHolder config) { + super(configPath, config); + this.title = title; + this.parent = parent; + + } + /** + * @return Get setting's gui title. + */ + @NotNull + public String getTitle() { + return title; + } + + /** + * @return The configured value for the associated setting. + */ + @NotNull + public abstract T getConfiguredValue(); + + @NotNull + public abstract List getDisplayLore(T value); + + /** + * @return Next value for a given enum + */ + @NotNull + public T next(@NotNull T now){ + Class clazz = now.getDeclaringClass(); + T[] values = clazz.getEnumConstants(); + + int index = now.ordinal(); + if(index == values.length - 1) + return values[0]; + + return values[index + 1]; + } + + /** + * Get default value value + * @return default value + */ + @NotNull + public abstract T getDefault(); + + @Override + public Gui create() { + // Get value or default + T now = getConfiguredValue(); + + // create new gui + return new EnumSettingGui<>(this, now); + } + + /** + * Create a new enum setting GuiItem. + * This item will create and open an enum setting GUI from the factory. + * + * @param name Name of the display. + * @return A formatted GuiItem that will create and open a GUI for the enum setting. + */ + public GuiItem getItem(@NotNull Material material, @NotNull String name) { + T value = getConfiguredValue(); + + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName(name); + meta.setLore(getDisplayLore(value)); + meta.addItemFlags(ItemFlag.values()); + + item.setItemMeta(meta); + + return GuiGlobalItems.openSettingGuiItem(item, this); + } + } + + public interface ConfigurableEnum { + + String configName(); + + ItemStack configurationGuiItem(); + String configurationGuiName(); + + + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/GroupSelectSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/GroupSelectSettingGui.java new file mode 100644 index 0000000..69986a0 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/GroupSelectSettingGui.java @@ -0,0 +1,157 @@ +package xyz.alexcrea.cuanvil.gui.config.settings; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.pane.Orientable; +import com.github.stefvanschie.inventoryframework.pane.OutlinePane; +import com.github.stefvanschie.inventoryframework.pane.Pane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.group.AbstractMaterialGroup; +import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui; +import xyz.alexcrea.cuanvil.gui.config.SelectGroupContainer; +import xyz.alexcrea.cuanvil.gui.config.list.ElementListConfigGui; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +public class GroupSelectSettingGui extends AbstractSettingGui { + + SelectGroupContainer groupContainer; + int page; + + Set selectedGroups; + + public GroupSelectSettingGui(@NotNull String title, ValueUpdatableGui parent, SelectGroupContainer groupContainer, int page) { + super(6, title, parent); + this.groupContainer = groupContainer; + //Not used but planned + this.page = page; + + this.selectedGroups = new HashSet<>(groupContainer.getSelectedGroups()); + + // Add secondary background item + this.getPane().bindItem('1', GuiSharedConstant.SECONDARY_BACKGROUND_ITEM); + + initGroups(); + } + + @Override + protected Pattern getGuiPattern() { + return new Pattern( + GuiSharedConstant.UPPER_FILLER_FULL_PLANE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + "B1111111S" + ); + } + + protected void initGroups() { + // Add enchantment gui item + OutlinePane filledEnchant = new OutlinePane( + ElementListConfigGui.LIST_FILLER_START_X, + ElementListConfigGui.LIST_FILLER_START_Y, + ElementListConfigGui.LIST_FILLER_LENGTH, + ElementListConfigGui.LIST_FILLER_HEIGHT); + filledEnchant.setPriority(Pane.Priority.HIGH); + filledEnchant.align(OutlinePane.Alignment.BEGIN); + filledEnchant.setOrientation(Orientable.Orientation.HORIZONTAL); + + Set illegalGroup = this.groupContainer.illegalGroups(); + for (AbstractMaterialGroup group : ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager().getGroupMap().values()) { + if (illegalGroup.contains(group)) { + continue; + } + filledEnchant.addItem(getGuiItemFromGroup(group)); + } + + addPane(filledEnchant); + + } + + private GuiItem getGuiItemFromGroup(AbstractMaterialGroup group) { + boolean isIn = this.selectedGroups.contains(group); + + Material usedMaterial = group.getRepresentativeMaterial(); + ItemStack item = new ItemStack(usedMaterial); + + setGroupItemMeta(item, group.getName(), isIn); + + GuiItem guiItem = new GuiItem(item, CustomAnvil.instance); + guiItem.setAction(getGroupItemConsumer(group, guiItem)); + return guiItem; + } + + private static final List TRUE_LORE = Collections.singletonList("§7Value: §aSelected"); + private static final List FALSE_LORE = Collections.singletonList("§7Value: §cNot Selected"); + + public void setGroupItemMeta(ItemStack item, String name, boolean isIn) { + ItemMeta meta = item.getItemMeta(); + + if (meta == null) { + CustomAnvil.instance.getLogger().warning("Could not create item for group: " + name + ":\n" + + "Item do not gave item meta: " + item + ". Using placeholder instead"); + item.setType(Material.PAPER); + meta = item.getItemMeta(); + assert meta != null; + } + + meta.setDisplayName("§" + (isIn ? 'a' : 'c') + CasedStringUtil.snakeToUpperSpacedCase(name)); + if (isIn) { + meta.addEnchant(Enchantment.DAMAGE_UNDEAD, 1, true); + meta.setLore(TRUE_LORE); + } else { + meta.removeEnchant(Enchantment.DAMAGE_UNDEAD); + meta.setLore(FALSE_LORE); + } + meta.addItemFlags(ItemFlag.values()); + + item.setItemMeta(meta); + } + + private Consumer getGroupItemConsumer(AbstractMaterialGroup group, GuiItem guiItem) { + return event -> { + event.setCancelled(true); + + boolean isIn = this.selectedGroups.contains(group); + if (isIn) { + this.selectedGroups.remove(group); + } else { + this.selectedGroups.add(group); + } + + ItemStack item = guiItem.getItem(); + setGroupItemMeta(item, group.getName(), !isIn); + guiItem.setItem(item);// Just in case + + update(); + }; + } + + @Override + public boolean onSave() { + return this.groupContainer.setSelectedGroups(this.selectedGroups); + } + + @Override + public boolean hadChange() { + Set baseGroup = this.groupContainer.getSelectedGroups(); + return baseGroup.size() != this.selectedGroups.size() || + !baseGroup.containsAll(this.selectedGroups); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/IntSettingsGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/IntSettingsGui.java new file mode 100644 index 0000000..73121a6 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/IntSettingsGui.java @@ -0,0 +1,409 @@ +package xyz.alexcrea.cuanvil.gui.config.settings; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +/** + * An instance of a gui used to edit an int setting. + */ +public class IntSettingsGui extends AbstractSettingGui { + + protected final IntSettingFactory holder; + protected final int before; + protected int now; + protected int step; + + /** + * Create an int setting config gui. + * + * @param holder Configuration factory of this setting. + * @param now The defined value of this setting. + */ + protected IntSettingsGui(IntSettingFactory holder, int now) { + super(3, holder.getTitle(), holder.parent); + assert holder.steps.length > 0 && holder.steps.length <= 9; + this.holder = holder; + this.before = now; + this.now = now; + this.step = holder.steps[0]; + + initStepsValue(); + prepareReturnToDefault(); + updateValueDisplay(); + } + + + @Override + public Pattern getGuiPattern() { + return new Pattern( + "abcdefghi", + "D0-0v0+00", + "B0000000S" + ); + } + + protected GuiItem returnToDefault; + + /** + * Prepare "return to default value" gui item. + */ + protected void prepareReturnToDefault() { + ItemStack item = new ItemStack(Material.COMMAND_BLOCK); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§eReset to default value"); + meta.setLore(Collections.singletonList("§7Default value is §e" + + holder.valueDisplayName(ValueDisplayType.RESET, holder.defaultVal))); + item.setItemMeta(meta); + returnToDefault = new GuiItem(item, event -> { + event.setCancelled(true); + now = holder.defaultVal; + updateValueDisplay(); + update(); + }, CustomAnvil.instance); + } + + /** + * Update item using the setting value to match the new value. + */ + protected void updateValueDisplay() { + PatternPane pane = getPane(); + + // minus item + GuiItem minusItem; + if (now > holder.min) { + int planned = Math.max(holder.min, now - step); + minusItem = valueEditItem(Material.RED_TERRACOTTA, ValueDisplayType.REMOVE, planned); + } else { + minusItem = GuiGlobalItems.backgroundItem(Material.BARRIER); + } + pane.bindItem('-', minusItem); + + //plus item + GuiItem plusItem; + if (now < holder.max) { + int planned = Math.min(holder.max, now + step); + plusItem = valueEditItem(Material.GREEN_TERRACOTTA, ValueDisplayType.ADD, planned); + } else { + plusItem = GuiGlobalItems.backgroundItem(Material.BARRIER); + } + pane.bindItem('+', plusItem); + + // "result" display + ItemStack resultPaper = new ItemStack(Material.PAPER); + ItemMeta resultMeta = resultPaper.getItemMeta(); + assert resultMeta != null; + + resultMeta.setDisplayName("§fValue: §e" + holder.valueDisplayName(ValueDisplayType.CURRENT, now)); + resultMeta.setLore(holder.displayLore); + + resultPaper.setItemMeta(resultMeta); + + GuiItem resultItem = new GuiItem(resultPaper, GuiGlobalActions.stayInPlace, CustomAnvil.instance); + + pane.bindItem('v', resultItem); + + // reset to default + GuiItem returnToDefault; + if (now != holder.defaultVal) { + returnToDefault = this.returnToDefault; + } else { + returnToDefault = GuiGlobalItems.backgroundItem(); + } + pane.bindItem('D', returnToDefault); + + } + + private GuiItem valueEditItem(Material mat, ValueDisplayType type, int planned) { + ItemStack item = new ItemStack(mat); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + var nowDisplay = holder.valueDisplayName(type, now); + var plannedDisplay = holder.valueDisplayName(type, planned); + var deltaDisplay = holder.deltaDisplay(type, now, planned); + meta.setDisplayName("§e" + nowDisplay + " §f-> §e" + plannedDisplay + " §r(§c" + deltaDisplay + "§r)"); + + meta.setLore(Collections.singletonList(AbstractSettingGui.CLICK_LORE)); + item.setItemMeta(meta); + return new GuiItem(item, updateNowConsumer(planned), CustomAnvil.instance); + } + + /** + * @param planned Value to change current setting to. + * @return A consumer to update the current setting's value. + */ + protected Consumer updateNowConsumer(int planned) { + return event -> { + event.setCancelled(true); + now = planned; + updateValueDisplay(); + update(); + }; + } + + /** + * Initialise step items. + */ + protected void initStepsValue() { + // Put background glass on the background of 'a' to 'b' + GuiItem background = GuiGlobalItems.backgroundItem(); + PatternPane pane = getPane(); + + for (char i = 'a'; i < (getMidStepChar() - 'a') * 2 + 1; i++) { + pane.bindItem(i, background); + } + // Then update legit step values + updateStepValue(); + } + + /** + * Update steps items value. + */ + protected void updateStepValue() { + if (holder.steps.length <= 1) return; + // We assume steps have a length of 2k+1 cause its more pretty + char val = getMidStepChar(); + // Offset to start (not the best way to do it) + val -= (char) ((holder.steps.length - 1) / 2); + + // Then place items + PatternPane pane = getPane(); + for (int i = 0; i < holder.steps.length; i++) { + pane.bindItem(val + i, stepGuiItem(i)); + } + + } + + /** + * Step use lower case character from 'a' to a certain char. + * + * @return The middle value of the character set for steps. + */ + protected char getMidStepChar() { + return 'e'; + } + + /** + * Create a step item from a step value. + * + * @param stepIndex the index of the step item. + * @return A step item corresponding to its index value. + */ + protected GuiItem stepGuiItem(int stepIndex) { + int stepValue = holder.steps[stepIndex]; + + // Get material properties + Material stepMat; + StringBuilder stepName = new StringBuilder("§"); + List stepLore; + Consumer clickEvent; + if (stepValue == step) { + stepMat = Material.GREEN_STAINED_GLASS_PANE; + stepName.append('a'); + stepLore = Collections.singletonList("§7Value is changing by " + stepValue); + clickEvent = GuiGlobalActions.stayInPlace; + } else { + stepMat = Material.RED_STAINED_GLASS_PANE; + stepName.append('c'); + stepLore = Collections.singletonList("§7Click here to change the value by " + stepValue); + clickEvent = updateStepValue(stepValue); + } + stepName.append("Step of: §e").append(stepValue); + + // Create item stack then gui item + ItemStack item = new ItemStack(stepMat); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName(stepName.toString()); + meta.setLore(stepLore); + item.setItemMeta(meta); + + return new GuiItem(item, clickEvent, CustomAnvil.instance); + } + + /** + * @param stepValue Value to change current step to. + * @return A consumer to update the current step of this setting. + */ + protected Consumer updateStepValue(int stepValue) { + return event -> { + event.setCancelled(true); + this.step = stepValue; + updateStepValue(); + updateValueDisplay(); + update(); + }; + } + + @Override + public boolean onSave() { + holder.config.getConfig().set(holder.configPath, now); + + if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) { + return holder.config.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + } + return true; + } + + @Override + public boolean hadChange() { + return now != before; + } + + /** + * A factory for an int setting gui that hold setting's information. + */ + public static class IntSettingFactory extends SettingGuiFactory { + + @NotNull + String title; + @NotNull + ValueUpdatableGui parent; + int min; + int max; + int defaultVal; + int[] steps; + + @NotNull + List displayLore; + + /** + * Constructor for an int setting gui factory. + * + * @param title The title of the gui. + * @param parent Parent gui to go back when completed. + * @param configPath Configuration path of this setting. + * @param config Configuration holder of this setting. + * @param displayLore Gui display item lore. + * @param min Minimum value of this setting. + * @param max Maximum value of this setting. + * @param defaultVal Default value if not found on the config. + * @param steps List of step the value can increment/decrement. + * List's size should be between 1 (included) and 5 (included). + * it is visually preferable to have an odd number of step. + * If step only contain 1 value, no step item should be displayed. + */ + public IntSettingFactory( + @NotNull String title, @NotNull ValueUpdatableGui parent, + @NotNull String configPath, @NotNull ConfigHolder config, + @Nullable List displayLore, + int min, int max, int defaultVal, int... steps) { + super(configPath, config); + this.title = title; + this.parent = parent; + this.min = min; + this.max = max; + this.defaultVal = defaultVal; + this.steps = steps; + + if(displayLore == null){ + this.displayLore = Collections.emptyList(); + }else { + this.displayLore = displayLore; + } + } + + /** + * @return Get setting's gui title + */ + @NotNull + public String getTitle() { + return title; + } + + /** + * @return The configured value for the associated setting. + */ + public int getConfiguredValue() { + return this.config.getConfig().getInt(this.configPath, this.defaultVal); + } + + @Override + public Gui create() { + // Get value or default + int now = getConfiguredValue(); + // create new gui + return new IntSettingsGui(this, now); + } + + /** + * Create a new int setting GuiItem. + * This item will create and open an int setting GUI from the factory. + * The item will have its value written in the lore part of the item. + * + * @param itemMat Displayed material of the item. + * @param name Name of the item. + * @return A formatted GuiItem that will create and open a GUI for the int setting. + */ + public GuiItem getItem( + @NotNull Material itemMat, + @NotNull String name + ) { + // Get item properties + int value = getConfiguredValue(); + StringBuilder itemName = new StringBuilder("§a").append(name); + + return GuiGlobalItems.createGuiItemFromProperties(this, itemMat, itemName, + "§e" + value, + this.displayLore, true); + } + + /** + * Create a new int setting GuiItem. + * This item will create and open an int setting GUI from the factory. + * The item will have its value written in the lore part of the item. + * Item's name will be the factory set title. + * + * @param itemMat Displayed material of the item. + * @return A formatted GuiItem that will create and open a GUI for the int setting. + */ + public GuiItem getItem( + @NotNull Material itemMat + ) { + String configPath = GuiGlobalItems.getConfigNameFromPath(getConfigPath()); + return getItem(itemMat, CasedStringUtil.detectToUpperSpacedCase(configPath)); + } + + protected String valueDisplayName(ValueDisplayType type, int value) { + return String.valueOf(value); + } + + protected String deltaDisplay(ValueDisplayType type, int now, int planned) { + var delta = planned - now; + if(delta < 0) return "§c" + delta; + else return "§a+" + delta; + } + + } + + public enum ValueDisplayType { + ADD, + CURRENT, + REMOVE, + RESET, + } + +} + diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/ItemSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/ItemSettingGui.java new file mode 100644 index 0000000..3df2af8 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/ItemSettingGui.java @@ -0,0 +1,270 @@ +package xyz.alexcrea.cuanvil.gui.config.settings; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.entity.HumanEntity; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +/** + * An instance of a gui used to edit an item setting. + */ +public class ItemSettingGui extends AbstractSettingGui { + + private final ItemSettingFactory holder; + private final ItemStack before; + private ItemStack now; + + /** + * Create an item setting config gui. + * + * @param holder Configuration factory of this setting. + * @param now The defined value of this setting. + */ + protected ItemSettingGui(ItemSettingFactory holder, ItemStack now) { + super(3, holder.getTitle(), holder.parent); + this.holder = holder; + this.before = now; + this.now = now; + + prepareStaticItems(); + updateValueDisplay(); + } + + @Override + public Pattern getGuiPattern() { + return new Pattern( + GuiSharedConstant.EMPTY_GUI_FULL_LINE, + "D0-0v0+0s", + "B0000000S" + ); + } + + + public void prepareStaticItems(){ + prepareReturnToDefault(); + + GuiItem temporaryLeave = GuiGlobalItems.temporaryCloseGuiToSelectItem(Material.YELLOW_STAINED_GLASS_PANE, this); + getPane().bindItem('s', temporaryLeave); + } + + + protected GuiItem returnToDefault; + + /** + * Prepare "return to default value" gui item. + */ + protected void prepareReturnToDefault() { + ItemStack item = new ItemStack(Material.COMMAND_BLOCK); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§eReset to default value"); + meta.setLore(Collections.singletonList("§7Default value is §e" + holder.defaultVal)); + item.setItemMeta(meta); + returnToDefault = new GuiItem(item, event -> { + event.setCancelled(true); + now = holder.defaultVal; + updateValueDisplay(); + update(); + }, CustomAnvil.instance); + } + + protected final static List CLICK_LORE = Collections.singletonList("§7Click Here with an item to change the value"); + + /** + * Update item using the setting value to match the new value + */ + protected void updateValueDisplay() { + PatternPane pane = getPane(); + + // Get displayed value for this config. + ItemStack displayedItem; + if(this.now != null){ + displayedItem = this.now.clone(); + }else{ + displayedItem = new ItemStack(Material.BARRIER); + ItemMeta valueMeta = displayedItem.getItemMeta(); + assert valueMeta != null; + + valueMeta.setDisplayName("§4NO ITEM SET"); + valueMeta.setLore(CLICK_LORE); + + displayedItem.setItemMeta(valueMeta); + } + + GuiItem resultItem = new GuiItem(displayedItem, setItemAsCursor(), CustomAnvil.instance); + pane.bindItem('v', resultItem); + + // reset to default + GuiItem returnToDefault; + if (now != holder.defaultVal) { + returnToDefault = this.returnToDefault; + } else { + returnToDefault = GuiGlobalItems.backgroundItem(); + } + pane.bindItem('D', returnToDefault); + + } + + /** + * @return A consumer to update the current setting's value. + */ + protected Consumer setItemAsCursor() { + return event -> { + event.setCancelled(true); + + HumanEntity player = event.getWhoClicked(); + ItemStack cursor = player.getItemOnCursor(); + + if(cursor.getType().isAir()) return; + + this.now = cursor; + + updateValueDisplay(); + update(); + }; + } + + @Override + public boolean onSave() { + holder.config.getConfig().set(holder.configPath, this.now); + + if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) { + return holder.config.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE); + } + return true; + } + + @Override + public boolean hadChange() { + if(now == null) { + return before != null; + } + + return !now.equals(before); + } + + /** + * A factory for an item setting gui that hold setting's information. + */ + public static class ItemSettingFactory extends SettingGuiFactory { + @NotNull + String title; + @NotNull + ValueUpdatableGui parent; + @Nullable + ItemStack defaultVal; + @NotNull + List displayLore; + + /** + * Constructor for an item setting gui factory. + * + * @param title The title of the gui. + * @param parent Parent gui to go back when completed. + * @param configPath Configuration path of this setting. + * @param config Configuration holder of this setting. + * @param defaultVal Default value if not found on the config. + * @param displayLore Gui display item lore. + */ + public ItemSettingFactory( + @NotNull String title, @NotNull ValueUpdatableGui parent, + @NotNull String configPath, @NotNull ConfigHolder config, + @Nullable ItemStack defaultVal, + String... displayLore) { + super(configPath, config); + this.title = title; + this.parent = parent; + + this.defaultVal = defaultVal; + this.displayLore = Arrays.asList(displayLore); + } + + /** + * @return Get setting's gui title. + */ + @NotNull + public String getTitle() { + return title; + } + + /** + * @return The configured value for the associated setting. + */ + public ItemStack getConfiguredValue() { + return this.config.getConfig().getItemStack(this.configPath, this.defaultVal); + } + + @NotNull + public List getDisplayLore() { + return this.displayLore; + } + + @Override + public Gui create() { + // Get current value or default + ItemStack now = getConfiguredValue(); + // create new gui + return new ItemSettingGui(this, now); + } + + /** + * Create a new item setting GuiItem. + * This item will create and open an item setting GUI from the factory. + * Item's name will be the factory set title. + * + * @param name Name of the item. + * @return A formatted GuiItem that will create and open a GUI for the item setting. + */ + public GuiItem getItem(@NotNull String name) { + ItemStack item = getConfiguredValue(); + if(item == null || item.getType().isAir()){ + item = new ItemStack(Material.BARRIER); + }else{ + item = item.clone(); + } + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§a" + name); + meta.setLore(getDisplayLore()); + meta.addItemFlags(ItemFlag.values()); + + item.setItemMeta(meta); + + return GuiGlobalItems.openSettingGuiItem(item, this); + } + + /** + * Create a new item setting GuiItem. + * This item will create and open an item setting GUI from the factory. + * Item's name will be the factory set title. + * + * @return A formatted GuiItem that will create and open a GUI for the item setting. + */ + public GuiItem getItem() { + String configPath = GuiGlobalItems.getConfigNameFromPath(getConfigPath()); + return getItem(CasedStringUtil.detectToUpperSpacedCase(configPath)); + } + + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/MaterialSelectSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/MaterialSelectSettingGui.java new file mode 100644 index 0000000..fc519ff --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/MaterialSelectSettingGui.java @@ -0,0 +1,309 @@ +package xyz.alexcrea.cuanvil.gui.config.settings; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.HumanEntity; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.gui.config.SelectMaterialContainer; +import xyz.alexcrea.cuanvil.gui.config.ask.ConfirmActionGui; +import xyz.alexcrea.cuanvil.gui.config.list.MappedElementListConfigGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems; +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; +import xyz.alexcrea.cuanvil.util.MaterialUtil; + +import java.util.*; +import java.util.function.Consumer; + +public class MaterialSelectSettingGui extends MappedElementListConfigGui { + + private final SelectMaterialContainer selector; + private final Gui backGui; + private boolean instantRemove; + + private final List defaultMaterials; + private final Set illegalMaterials; + private final int defaultMaterialHash; + private int nowMaterialHash; + + public MaterialSelectSettingGui( + @NotNull SelectMaterialContainer selector, + @NotNull String title, + @NotNull Gui backGui) { + super(title); + this.selector = selector; + this.backGui = backGui; + this.instantRemove = false; + + this.defaultMaterials = new ArrayList<>(this.selector.getSelectedMaterials()); + this.illegalMaterials = this.selector.illegalMaterials(); + + this.defaultMaterialHash = hashFromMaterialList(this.defaultMaterials); + this.nowMaterialHash = this.defaultMaterialHash; + + init(); + + // Change back item + this.backgroundPane.bindItem('B', GuiGlobalItems.backItem(backGui)); + } + + @Override + protected Pattern getBackgroundPattern(){ + return new Pattern( + GuiSharedConstant.UPPER_FILLER_FULL_PLANE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + "BT1LAR1IS" + ); + } + + private GuiItem saveItem; + private GuiItem noChangeItem; + + private GuiItem instantRemoveOn; + private GuiItem instantRemoveOff; + + @Override + protected void prepareStaticValues() { + super.prepareStaticValues(); + + // Temporary leave item + GuiItem temporaryLeave = GuiGlobalItems.temporaryCloseGuiToSelectItem(Material.YELLOW_STAINED_GLASS_PANE, this); + this.backgroundPane.bindItem('T', temporaryLeave); + + // Select new mat item + ItemStack selectItem = new ItemStack(Material.BLUE_STAINED_GLASS_PANE); + ItemMeta selectMeta = selectItem.getItemMeta(); + assert selectMeta != null; + + selectMeta.setDisplayName("§aAdd Item"); + selectMeta.setLore(Arrays.asList( + "§7Click here with an item to add", + "§7it's Material to the list.")); + + selectItem.setItemMeta(selectMeta); + + this.backgroundPane.bindItem('A', new GuiItem(selectItem, setItemAsCursor(), CustomAnvil.instance)); + + // Save item + this.saveItem = prepareSaveItem(); + + this.noChangeItem = GuiGlobalItems.noChangeItem(); + this.backgroundPane.bindItem('S', this.noChangeItem); + + // Instant Remove On item + ItemStack instantRemoveOnItem = new ItemStack(Material.LIME_STAINED_GLASS_PANE); + ItemMeta instantRemoveOnMeta = instantRemoveOnItem.getItemMeta(); + assert instantRemoveOnMeta != null; + + instantRemoveOnMeta.setDisplayName("§eInstant remove is §aEnabled §e!"); + instantRemoveOnMeta.setLore( + Collections.singletonList("§7Click here to disable the instant remove")); + + instantRemoveOnItem.setItemMeta(instantRemoveOnMeta); + + // Instant Remove Off item + ItemStack instantRemoveOffItem = new ItemStack(Material.RED_STAINED_GLASS_PANE); + ItemMeta instantRemoveOffMeta = instantRemoveOffItem.getItemMeta(); + assert instantRemoveOffMeta != null; + + instantRemoveOffMeta.setDisplayName("§eInstant remove is §cDisabled §e!"); + instantRemoveOffMeta.setLore( + Collections.singletonList("§7Click here to enable the instant remove")); + + instantRemoveOffItem.setItemMeta(instantRemoveOffMeta); + + // Instant Remove gui items + this.instantRemoveOn = new GuiItem(instantRemoveOnItem, (event -> { + this.instantRemove = false; + this.backgroundPane.bindItem('I', this.instantRemoveOff); + update(); + }), CustomAnvil.instance); + + this.instantRemoveOff = new GuiItem(instantRemoveOffItem, (event -> { + this.instantRemove = true; + this.backgroundPane.bindItem('I', this.instantRemoveOn); + update(); + }), CustomAnvil.instance); + + this.backgroundPane.bindItem('I', this.instantRemoveOff); + } + + private GuiItem prepareSaveItem() { + ItemStack saveItemStack = new ItemStack(GuiGlobalItems.DEFAULT_SAVE_ITEM); + ItemMeta saveMeta = saveItemStack.getItemMeta(); + assert saveMeta != null; + + saveMeta.setDisplayName("§aSave"); + + saveItemStack.setItemMeta(saveMeta); + + return new GuiItem(saveItemStack, event -> { + event.setCancelled(true); + + HumanEntity player = event.getWhoClicked(); + // Do not allow to save configuration if player do not have edit configuration permission + if (!player.hasPermission(CustomAnvil.editConfigPermission)) { + player.closeInventory(); + player.sendMessage(GuiGlobalActions.NO_EDIT_PERM); + return; + } + if(testCantSave()) return; + + + // Save setting + Set result = new HashSet<>(this.elementGuiMap.keySet()); + + if(!this.selector.setSelectedMaterials(result)){ + player.sendMessage("§cSomething went wrong while saving the change of value."); + } + + // Return to parent + this.backGui.show(player); + + }, CustomAnvil.instance); + } + + /** + * @return A consumer to update the current setting's value. + */ + protected Consumer setItemAsCursor() { + return event -> { + event.setCancelled(true); + + HumanEntity player = event.getWhoClicked(); + ItemStack cursor = player.getItemOnCursor(); + + // Test if cursor material allowed + NamespacedKey cursorMat = MaterialUtil.INSTANCE.getCustomType(cursor); + if(MaterialUtil.INSTANCE.isAir(cursorMat)) return; + if(this.illegalMaterials.contains(cursorMat)) return; + + // Update gui only if item did not exist before. + if(!this.elementGuiMap.containsKey(cursorMat)){ + updateValueForGeneric(cursorMat, true); + this.nowMaterialHash ^= cursorMat.hashCode(); + + setSaveItem(); + update(); + } + }; + } + + @Override + protected ItemStack createItemForGeneric(NamespacedKey material) { + ItemStack item = new ItemStack(Objects.requireNonNull(MaterialUtil.INSTANCE.getMatFromKey(material))); + ItemMeta meta = item.getItemMeta(); + + if(meta == null) return item; + meta.setDisplayName("§a" + CasedStringUtil.snakeToUpperSpacedCase(material.getKey().toLowerCase())); + meta.setLore(Collections.singletonList("§7Click here to remove this material from the list")); + meta.addItemFlags(ItemFlag.values()); + + item.setItemMeta(meta); + + return item; + } + + @Override + protected Collection getEveryDisplayableInstanceOfGeneric() { + return this.defaultMaterials; + } + + @Override + protected void updateElement(NamespacedKey material, GuiItem element) { + // Nothing happen here I think + } + + @Override + protected GuiItem newElementRequested(NamespacedKey material, GuiItem newItem) { + newItem.setAction(event -> { + if(this.instantRemove){ + removeMaterial(material); + }else { + String materialName = CasedStringUtil.snakeToUpperSpacedCase(material.getKey().toLowerCase()); + + // Create and show confirm remove gui. + ConfirmActionGui confirmGui = new ConfirmActionGui( + "Remove " + materialName, + "§7Confirm Remove " + materialName.toLowerCase() + " from this list.", + this, this, + () -> { + removeMaterial(material); + return true; + }, false + ); + confirmGui.show(event.getWhoClicked()); + + } + }); + return newItem; + } + + private void removeMaterial(NamespacedKey material) { + if(this.elementGuiMap.containsKey(material)){ + this.nowMaterialHash ^= material.hashCode(); + setSaveItem(); + removeGeneric(material); + } + + } + + @Override + protected GuiItem findItemFromElement(NamespacedKey generic, GuiItem element) { + return element; + } + + @Override + protected GuiItem findGuiItemForRemoval(NamespacedKey generic, GuiItem element) { + return element; + } + + private static int hashFromMaterialList(List materialList){ + int defaultMaterialHash = 0; + for (NamespacedKey material : materialList) { + defaultMaterialHash ^= material.hashCode(); + } + return defaultMaterialHash; + } + + private void setSaveItem() { + if(testCantSave()){ + this.backgroundPane.bindItem('S', this.noChangeItem); + }else{ + this.backgroundPane.bindItem('S', this.saveItem); + } + + } + + private boolean testCantSave() { + return this.defaultMaterialHash == this.nowMaterialHash; + } + + + // Unused functions. + @Override + protected GuiItem prepareCreateNewItem() {// Not used + return null; + } + @Override + protected Consumer prepareCreateItemConsumer(HumanEntity player) {// Not used + return null; + } + + @Override + protected String genericDisplayedName() {// Not Used + return null; + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/SettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/SettingGui.java new file mode 100644 index 0000000..a6d2784 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/SettingGui.java @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.gui.config.settings; + +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; + +public interface SettingGui { + + /** + * Called when the associated setting need to be saved. + * + * @return true if the save was successful. false otherwise + */ + boolean onSave(); + + /** + * If this function return true + * the gui assume the associated setting can be saved. + * + * @return true if there is a change to the setting. false otherwise + */ + boolean hadChange(); + + interface SettingGuiFactory { + + /** + * Create a gui using setting parameters and current setting value. + * + * @return A new instance of the implemented setting gui. + */ + Gui create(); + + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/WorkPenaltyTypeSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/WorkPenaltyTypeSettingGui.java new file mode 100644 index 0000000..4345aa1 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/WorkPenaltyTypeSettingGui.java @@ -0,0 +1,252 @@ +package xyz.alexcrea.cuanvil.gui.config.settings; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import io.delilaheve.CustomAnvil; +import io.delilaheve.util.ConfigOptions; +import org.bukkit.Material; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.HumanEntity; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.anvil.AnvilUseType; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.config.WorkPenaltyType; +import xyz.alexcrea.cuanvil.gui.config.global.BasicConfigGui; +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class WorkPenaltyTypeSettingGui extends AbstractSettingGui { + + private static final String INCREASING_EXPLANATION = "§eIncreasing§7: will penalty be increased (in item)"; + private static final String ADDING_EXPLANATION = "§eAdditive§7: will penalty be added to the cost"; + + private static final String SHARED_EXPLANATION = "§eShared§7: Vanilla, shared penalty. it will be kept from before the plugin installation."; + private static final String EXCLUSIVE_EXPLANATION = "§eExclusive§7: Custom, per anvil use type penalty. it will be lost after plugin uninstallation"; + + private final @NotNull WorkPenaltyType currentType; + private final @NotNull Map items; + + public WorkPenaltyTypeSettingGui(@NotNull BasicConfigGui parent) { + super(4, "§8Work Penalty Type", parent); + + this.currentType = ConfigOptions.INSTANCE.getWorkPenaltyType(); + this.items = new EnumMap<>(this.currentType.getPartMap()); + + for (AnvilUseType type : useTypes.keySet()) { + updateGuiForType(type); + } + } + + public static GuiItem getDisplayItem(@NotNull BasicConfigGui parent, + @NotNull Material itemMat, + @NotNull String name) { + List displayLore = new ArrayList<>(); + + displayLore.add("§7Work penalty increase the price for every anvil use."); + displayLore.add("§7This config allow you to choose the comportment of work penalty."); + displayLore.add(INCREASING_EXPLANATION); + displayLore.add(ADDING_EXPLANATION); + displayLore.add(""); + displayLore.add("§7About shared/exclusive penalty:"); + displayLore.add(SHARED_EXPLANATION); + displayLore.add(EXCLUSIVE_EXPLANATION); + + ItemStack item = new ItemStack(itemMat); + + ItemMeta meta = item.getItemMeta(); + meta.setDisplayName(name); + meta.setLore(displayLore); + + item.setItemMeta(meta); + + return new GuiItem(item, (event) -> { + event.setCancelled(true); + HumanEntity player = event.getWhoClicked(); + + // Do not allow to open inventory if player do not have edit configuration permission + if (!player.hasPermission(CustomAnvil.editConfigPermission)) { + player.closeInventory(); + player.sendMessage(GuiGlobalActions.NO_EDIT_PERM); + return; + } + new WorkPenaltyTypeSettingGui(parent).show(player); + }, CustomAnvil.instance); + } + + private static final Map useTypes = + Map.of( + AnvilUseType.RENAME_ONLY, "a1z9Z", + AnvilUseType.MERGE, "b2y8Y", + AnvilUseType.UNIT_REPAIR, "c3x7X", + AnvilUseType.CUSTOM_CRAFT, "d4w6W" + ); + + @Override + protected Pattern getGuiPattern() { + return new Pattern( // Yeah that a mess + "00a1z9Z00", + "00b2y8Y00", + "00c3x7X00", + "B004w600S" + ); + } + + public void updateGuiForType(AnvilUseType type) { + PatternPane pane = getPane(); + + String typeVals = useTypes.get(type); + + char increment = typeVals.charAt(0); + char additive = typeVals.charAt(1); + char display = typeVals.charAt(2); + char exclusiveIncrement = typeVals.charAt(3); + char exclusiveAdditive = typeVals.charAt(4); + + WorkPenaltyType.WorkPenaltyPart part = items.get(type); + String increasingStr = (part.penaltyIncrease() ? "§a" : "§c") + "Increasing"; + String additiveStr = (part.penaltyAdditive() ? "§a" : "§c") + "Additive"; + String exclusiveIncreasingStr = (part.exclusivePenaltyIncrease() ? "§a" : "§c") + "Increasing"; + String exclusiveAdditiveStr = (part.exclusivePenaltyAdditive() ? "§a" : "§c") + "Additive"; + + // Display item + ItemStack displayItem = new ItemStack(type.getDisplayMat()); + + ArrayList displayLore = new ArrayList<>(); + displayLore.add("§eShared§7: " + additiveStr + " §7| " + increasingStr); + displayLore.add("§eExclusive§7: " + exclusiveAdditiveStr + " §7| " + exclusiveIncreasingStr); + + ItemMeta meta = displayItem.getItemMeta(); + meta.setDisplayName("§e" + type.getDisplayName()); + meta.setLore(displayLore); + displayItem.setItemMeta(meta); + + pane.bindItem(display, new GuiItem(displayItem, (event) -> { + event.setCancelled(true); + })); + + // Can probably put this in a function but this works so + // "Increment" item + ItemStack incrementItem = new ItemStack(part.penaltyIncrease() ? Material.GREEN_TERRACOTTA : Material.RED_TERRACOTTA); + + meta = incrementItem.getItemMeta(); + meta.setDisplayName(increasingStr); + meta.setLore(List.of(INCREASING_EXPLANATION)); + meta.setLore(List.of(SHARED_EXPLANATION)); + incrementItem.setItemMeta(meta); + + pane.bindItem(increment, new GuiItem(incrementItem, (event) -> { + event.setCancelled(true); + + WorkPenaltyType.WorkPenaltyPart newPart = new WorkPenaltyType.WorkPenaltyPart( + !part.penaltyIncrease(), part.penaltyAdditive(), + part.exclusivePenaltyIncrease(), part.exclusivePenaltyAdditive()); + items.replace(type, newPart); + updateGuiForType(type); + update(); + })); + + // "Additive" item + ItemStack additiveItem = new ItemStack(part.penaltyAdditive() ? Material.GREEN_TERRACOTTA : Material.RED_TERRACOTTA); + + meta = additiveItem.getItemMeta(); + meta.setDisplayName(additiveStr); + meta.setLore(List.of(ADDING_EXPLANATION)); + meta.setLore(List.of(SHARED_EXPLANATION)); + additiveItem.setItemMeta(meta); + + pane.bindItem(additive, new GuiItem(additiveItem, (event) -> { + event.setCancelled(true); + + WorkPenaltyType.WorkPenaltyPart newPart = new WorkPenaltyType.WorkPenaltyPart( + part.penaltyIncrease(), !part.penaltyAdditive(), + part.exclusivePenaltyIncrease(), part.exclusivePenaltyAdditive()); + items.replace(type, newPart); + updateGuiForType(type); + update(); + })); + + // exclusive "Increment" item + ItemStack exclusiveIncrementItem = new ItemStack(part.exclusivePenaltyIncrease() ? Material.GREEN_TERRACOTTA : Material.RED_TERRACOTTA); + + meta = exclusiveIncrementItem.getItemMeta(); + meta.setDisplayName(exclusiveIncreasingStr); + meta.setLore(List.of(INCREASING_EXPLANATION)); + meta.setLore(List.of(EXCLUSIVE_EXPLANATION)); + exclusiveIncrementItem.setItemMeta(meta); + + pane.bindItem(exclusiveIncrement, new GuiItem(exclusiveIncrementItem, (event) -> { + event.setCancelled(true); + + WorkPenaltyType.WorkPenaltyPart newPart = new WorkPenaltyType.WorkPenaltyPart( + part.penaltyIncrease(), part.penaltyAdditive(), + !part.exclusivePenaltyIncrease(), part.exclusivePenaltyAdditive()); + items.replace(type, newPart); + updateGuiForType(type); + update(); + })); + + // exclusive "Additive" item + ItemStack exclusiveAdditiveItem = new ItemStack(part.exclusivePenaltyAdditive() ? Material.GREEN_TERRACOTTA : Material.RED_TERRACOTTA); + + meta = exclusiveAdditiveItem.getItemMeta(); + meta.setDisplayName(exclusiveAdditiveStr); + meta.setLore(List.of(ADDING_EXPLANATION)); + meta.setLore(List.of(EXCLUSIVE_EXPLANATION)); + exclusiveAdditiveItem.setItemMeta(meta); + + pane.bindItem(exclusiveAdditive, new GuiItem(exclusiveAdditiveItem, (event) -> { + event.setCancelled(true); + + WorkPenaltyType.WorkPenaltyPart newPart = new WorkPenaltyType.WorkPenaltyPart( + part.penaltyIncrease(), part.penaltyAdditive(), + part.exclusivePenaltyIncrease(), !part.exclusivePenaltyAdditive()); + items.replace(type, newPart); + updateGuiForType(type); + update(); + })); + } + + @Override + public boolean onSave() { + return saveWorkPenalty(items); + } + + public static boolean saveWorkPenalty(Map partEnum) { + ConfigHolder configHolder = ConfigHolder.DEFAULT_CONFIG; + FileConfiguration config = configHolder.getConfig(); + + partEnum.forEach((key, value) -> { + String partPath = key.getPath(); + + if (key.getDefaultPenalty().equals(value)) { + config.set(partPath, null); + return; + } + + config.set(partPath + '.' + ConfigOptions.WORK_PENALTY_INCREASE, value.penaltyIncrease()); + config.set(partPath + '.' + ConfigOptions.WORK_PENALTY_ADDITIVE, value.penaltyAdditive()); + config.set(partPath + '.' + ConfigOptions.EXCLUSIVE_WORK_PENALTY_INCREASE, value.exclusivePenaltyIncrease()); + config.set(partPath + '.' + ConfigOptions.EXCLUSIVE_WORK_PENALTY_ADDITIVE, value.exclusivePenaltyAdditive()); + }); + + return configHolder.saveToDisk(true); + } + + @Override + public boolean hadChange() { + for (AnvilUseType type : items.keySet()) { + if (!currentType.getPenaltyInfo(type).equals(items.get(type))) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/util/GuiGlobalActions.java b/src/main/java/xyz/alexcrea/cuanvil/gui/util/GuiGlobalActions.java new file mode 100644 index 0000000..694fa14 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/util/GuiGlobalActions.java @@ -0,0 +1,145 @@ +package xyz.alexcrea.cuanvil.gui.util; + +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import io.delilaheve.CustomAnvil; +import org.bukkit.entity.HumanEntity; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui; +import xyz.alexcrea.cuanvil.gui.config.settings.SettingGui; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.function.Consumer; + +/** + * A utility class to store function that create generic GUI actions. + */ +public class GuiGlobalActions { + + public static final String NO_EDIT_PERM = "§cYou do not have permission to edit the config"; + + /** + * A Consumer that should be used if the item goal is to do nothing on click. + */ + public static final Consumer stayInPlace = event -> event.setCancelled(true); + + /** + * Create a consumer to create and open a new GUI. + * Used with InventoryClickEvent as the consumer argument as it is planned to be used on click on an GuiItem. + * + * @param clazz The class of the gui to open. + * It is assumed this class contain a constructor requiring arguments of argClass in the same order as argClass array. + * @param argClass Classes of the argument that will be passed to the constructor of the GUI class. + * @param args Arguments for the constructor the GUI class. + * @return A consumer to create a new gui and open it. + */ + public static @NotNull Consumer openGuiAction( + @NotNull Class clazz, + @NotNull Class[] argClass, + @NotNull Object... args) { + return event -> { + event.setCancelled(true); + HumanEntity player = event.getWhoClicked(); + // Do not allow to open inventory if player do not have edit configuration permission + if (!player.hasPermission(CustomAnvil.editConfigPermission)) { + player.closeInventory(); + player.sendMessage(NO_EDIT_PERM); + return; + } + try { + Constructor constructor = clazz.getConstructor(argClass); + // Assume constructor is accessible + Gui gui = constructor.newInstance(args); + gui.show(player); + + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | + InstantiationException e) { + throw new RuntimeException(e); + } + }; + } + + /** + * Create a consumer to create and open a new GUI. + * Used with InventoryClickEvent as the consumer argument as it is planned to be used on click on an GuiItem. + * + * @param clazz The class of the gui to open. + * It is assumed this class contain a constructor with no argument. + * @return A consumer to create a new gui and open it. + */ + public static @NotNull Consumer openGuiAction( + @NotNull Class clazz) { + return openGuiAction(clazz, new Class[0]); + } + + /** + * Create a consumer to open a setting gui from a setting GUI factory. + * Used with InventoryClickEvent as the consumer argument as it is planned to be used on click on an GuiItem. + * + * @param factory The setting gui factory. + * @return A consumer to create and open a new setting GUI. + */ + public static @NotNull Consumer openSettingGuiAction(SettingGui.SettingGuiFactory factory) { + return event -> { + event.setCancelled(true); + Gui gui = factory.create(); + gui.show(event.getWhoClicked()); + }; + } + + /** + * Create a consumer to open a global GUI. + * Used with InventoryClickEvent as the consumer argument as it is planned to be used on click on an GuiItem. + * + * @param goal The gui to open when consumer is run. + * @return A consumer to open a global GUI. + */ + public static @NotNull Consumer openGuiAction(@NotNull Gui goal) { + return event -> { + event.setCancelled(true); + HumanEntity player = event.getWhoClicked(); + // Do not allow to open inventory if player do not have edit configuration permission + if (!player.hasPermission(CustomAnvil.editConfigPermission)) { + player.closeInventory(); + player.sendMessage(NO_EDIT_PERM); + return; + } + goal.show(player); + }; + } + + /** + * Create a consumer to update and open an updatable GUI. + * Used with InventoryClickEvent as the consumer argument as it is planned to be used on click on an GuiItem. + * This consumer check if the player who interacted with the item have the permission to save before saving. + * + * @param setting The gui that contain the modified setting. + * @param goal The gui to update and open when consumer is run. + * @return A consumer to open a global GUI. + */ + public static @NotNull Consumer saveSettingAction( + @NotNull SettingGui setting, + @NotNull ValueUpdatableGui goal) { + return event -> { + event.setCancelled(true); + HumanEntity player = event.getWhoClicked(); + // Do not allow to save configuration if player do not have edit configuration permission + if (!player.hasPermission(CustomAnvil.editConfigPermission)) { + player.closeInventory(); + player.sendMessage(NO_EDIT_PERM); + return; + } + + // Save setting + if (!setting.onSave()) { + player.sendMessage("§cSomething went wrong while saving the change of value."); + } + // Update gui for those who have it open. + goal.updateGuiValues(); + // Then show + goal.getConnectedGui().show(player); + }; + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/util/GuiGlobalItems.java b/src/main/java/xyz/alexcrea/cuanvil/gui/util/GuiGlobalItems.java new file mode 100644 index 0000000..79f1462 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/util/GuiGlobalItems.java @@ -0,0 +1,271 @@ +package xyz.alexcrea.cuanvil.gui.util; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.util.Gui; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.entity.HumanEntity; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui; +import xyz.alexcrea.cuanvil.gui.config.settings.SettingGui; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A utility class to store function that create generic GUI item. + */ +public class GuiGlobalItems { + + // statically create default back itemstack + private static final ItemStack BACK_ITEM; + + static { + BACK_ITEM = new ItemStack(Material.BARRIER); + ItemMeta meta = BACK_ITEM.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§cBack"); + BACK_ITEM.setItemMeta(meta); + } + + /** + * Create a GuiItem that open the given GUi. + * + * @param item The item to display in the GUI. + * @param goal The GUI to open on click. + * @return An GuiItem that open goal on click. + */ + public static GuiItem goToGuiItem(@NotNull ItemStack item, @NotNull Gui goal) { + return new GuiItem(item, GuiGlobalActions.openGuiAction(goal), CustomAnvil.instance); + } + + /** + * Create back button item from default back GuiItem. + * The back item will open the goal inventory when clicked. + * + * @param goal The GUI to go back to. + * @return An GuiItem that go back to goal on click. + */ + public static GuiItem backItem(@NotNull Gui goal) { + return goToGuiItem(BACK_ITEM, goal); + } + + /** + * Add default back item to a GUI pattern with the reserved character key B. + * The back item will open the target inventory when clicked. + * + * @param target The pattern to add the back item. + * @param goal The GUI to go back to. + */ + public static void addBackItem(@NotNull PatternPane target, + @NotNull Gui goal) { + target.bindItem('B', backItem(goal)); + } + + private static final Material DEFAULT_BACKGROUND_MAT = Material.LIGHT_GRAY_STAINED_GLASS_PANE; + + /** + * Get a background item with backgroundMat as the displayed material. + * A background item is a GuiItem that do nothing when interacted with and have an empty name. + * + * @param backgroundMat The material to which the background item should be made of. + * @return A background item with backgroundMat as material. + */ + public static GuiItem backgroundItem(Material backgroundMat) { + ItemStack item = new ItemStack(backgroundMat); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§c"); + item.setItemMeta(meta); + return new GuiItem(item, GuiGlobalActions.stayInPlace, CustomAnvil.instance); + } + + /** + * Get default background GuiItem. + * A background item is a GuiItem that do nothing when interacted with and have an empty name. + * + * @return A new instance of the default background item. + */ + public static GuiItem backgroundItem() { + return backgroundItem(DEFAULT_BACKGROUND_MAT); + } + + /** + * Add default background item to a GUI pattern with the reserved character key 0. + * A background item is a GuiItem that do nothing when interacted with and have an empty name. + * + * @param target The pattern to add the background item. + * @param backgroundMat The material of the background item. + */ + public static void addBackgroundItem(@NotNull PatternPane target, + @NotNull Material backgroundMat) { + target.bindItem('0', backgroundItem(backgroundMat)); + } + + /** + * Add default background item to a GUI pattern with the reserved character key 0. + * A background item is a GuiItem that do nothing when interacted with and have an empty name. + * + * @param target The pattern to add the background item. + */ + public static void addBackgroundItem(@NotNull PatternPane target) { + addBackgroundItem(target, DEFAULT_BACKGROUND_MAT); + } + + public static final Material DEFAULT_SAVE_ITEM = Material.LIME_DYE; + public static final Material DEFAULT_NO_CHANGE_ITEM = Material.GRAY_DYE; + + /** + * Create a new save setting GuiItem. + * A save setting item is a GuiItem that save a changed setting when clicked. + * This item also check if the player who interacted with the item have the permission to save before saving. + * + * @param setting The setting to change. + * @param goal Parent GUI of this setting GUI. as setting will be change the display of goal GUI will be updated. + * @return A save setting item. + */ + public static GuiItem saveItem( + @NotNull SettingGui setting, + @NotNull ValueUpdatableGui goal) { + + ItemStack item = new ItemStack(DEFAULT_SAVE_ITEM); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§aSave"); + item.setItemMeta(meta); + return new GuiItem(item, + GuiGlobalActions.saveSettingAction(setting, goal), + CustomAnvil.instance); + } + + // Create static non change item + private static final GuiItem NO_CHANGE_ITEM; + + static { + ItemStack item = new ItemStack(DEFAULT_NO_CHANGE_ITEM); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§7No change. can't save."); + item.setItemMeta(meta); + NO_CHANGE_ITEM = new GuiItem(item, GuiGlobalActions.stayInPlace, CustomAnvil.instance); + } + + /** + * Get the global "no change" GuiItem. + * The no change item do nothing when interacted, only the title is change to show there is no change. + * + * @return The global "no change" item. + */ + public static GuiItem noChangeItem() { + return NO_CHANGE_ITEM; + } + + /** + * Create a new "create and go to the setting GUI" GuiItem. + * This item will create and open a setting GUI from the factory. + * + * @param item The item that will be displayed. + * @param factory The setting's GUI factory. + * @return A formatted GuiItem that will create and open a GUI for the setting. + */ + public static GuiItem openSettingGuiItem( + @NotNull ItemStack item, + @NotNull SettingGui.SettingGuiFactory factory + ) { + return new GuiItem(item, GuiGlobalActions.openSettingGuiAction(factory), CustomAnvil.instance); + } + + // Prefix of the one line lore that will be added to setting's item. + public static final String SETTING_ITEM_LORE_PREFIX = "§7value: "; + + /** + * Create an arbitrary GuiItem from a unique setting and item's property. + * + * @param factory The setting's GUI factory. + * @param itemMat Displayed material of the item. + * @param itemName Name of the item. + * @param value Value of the setting when the item is created. + * Will not update automatically, if the setting's value change, the item need to be created again. + * @param displayLore Gui display item lore. + * @return A formatted GuiItem that will create and open a GUI for the setting. + */ + public static GuiItem createGuiItemFromProperties( + @NotNull SettingGui.SettingGuiFactory factory, + @NotNull Material itemMat, + @NotNull StringBuilder itemName, + @NotNull Object value, + @NotNull List displayLore, + boolean displayValuePrefix + ) { + // Prepare lore + ArrayList lore = new ArrayList<>(); + lore.add((displayValuePrefix ? SETTING_ITEM_LORE_PREFIX : "") + value); + if(!displayLore.isEmpty()){ + lore.add(""); + lore.addAll(displayLore); + } + + // Create & initialise item + ItemStack item = new ItemStack(itemMat); + ItemMeta itemMeta = item.getItemMeta(); + assert itemMeta != null; + + itemMeta.setDisplayName(itemName.toString()); + itemMeta.setLore(lore); + itemMeta.addItemFlags(ItemFlag.values()); + + item.setItemMeta(itemMeta); + // Create GuiItem + return openSettingGuiItem(item, factory); + } + + /** + * Get the setting name from the setting path. + * For example: "gui.command.name" will return "name". + * + * @param path The setting's path. + * @return The setting's name. + */ + public static String getConfigNameFromPath(String path) { + // Get index of first dot + int indexOfDot = path.indexOf("."); + // when indexOfDot == -1 (not fond), it is implied that indexOfDot+1 = 0. substring will keep the full path as expected + return path.substring(indexOfDot + 1); + } + + public static GuiItem temporaryCloseGuiToSelectItem(Material itemMaterial, Gui openBack){ + ItemStack item = new ItemStack(itemMaterial); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§eTemporary close this menu"); + meta.setLore(Collections.singletonList("§7Allow you to chose other item then return here.")); + item.setItemMeta(meta); + + return new GuiItem(item, event -> { + event.setCancelled(true); + + HumanEntity player = event.getWhoClicked(); + + CustomAnvil.Companion.getChatListener().setListenedCallback(player, (message) ->{ + + if(message == null) return; + openBack.show(player); + + }); + + player.sendMessage("§eWrite something in chat to return to the item config menu."); + player.closeInventory(); + }, CustomAnvil.instance); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/util/GuiSharedConstant.java b/src/main/java/xyz/alexcrea/cuanvil/gui/util/GuiSharedConstant.java new file mode 100644 index 0000000..11f3657 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/util/GuiSharedConstant.java @@ -0,0 +1,83 @@ +package xyz.alexcrea.cuanvil.gui.util; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.pane.Pane; +import com.github.stefvanschie.inventoryframework.pane.PatternPane; +import com.github.stefvanschie.inventoryframework.pane.util.Pattern; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import xyz.alexcrea.cuanvil.gui.config.MainConfigGui; + +import java.util.Arrays; +import java.util.Collections; + +public class GuiSharedConstant { + + private GuiSharedConstant(){} + + public static final Material SECONDARY_BACKGROUND_MATERIAL = Material.BLACK_STAINED_GLASS_PANE; + public static final GuiItem SECONDARY_BACKGROUND_ITEM = GuiGlobalItems.backgroundItem(GuiSharedConstant.SECONDARY_BACKGROUND_MATERIAL); + + public static final String UPPER_FILLER_FULL_PLANE = "111111111"; + public static final String EMPTY_GUI_FULL_LINE = "000000000"; + public static final String EMPTY_FILLER_FULL_LINE = "100000001"; + + // Temporary values, until I get something better. + public static final boolean TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE = true; + public static final boolean TEMPORARY_DO_BACKUP_EVERY_SAVE = true; + + public static final PatternPane BACK_TO_MAIN_MENU_BIG_LIST_DISPLAY_BACKGROUND_PANE; + + static { + Pattern pattern = new Pattern( + GuiSharedConstant.UPPER_FILLER_FULL_PLANE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + GuiSharedConstant.EMPTY_FILLER_FULL_LINE, + "B11111111" + ); + BACK_TO_MAIN_MENU_BIG_LIST_DISPLAY_BACKGROUND_PANE = new PatternPane(0, 0, 9, 6, Pane.Priority.LOW, pattern); + + GuiGlobalItems.addBackItem(BACK_TO_MAIN_MENU_BIG_LIST_DISPLAY_BACKGROUND_PANE, MainConfigGui.getInstance()); + + GuiGlobalItems.addBackgroundItem(BACK_TO_MAIN_MENU_BIG_LIST_DISPLAY_BACKGROUND_PANE); + BACK_TO_MAIN_MENU_BIG_LIST_DISPLAY_BACKGROUND_PANE.bindItem('1', GuiSharedConstant.SECONDARY_BACKGROUND_ITEM); + + } + + public static final ItemStack CANCEL_ITEM; + public static final ItemStack CONFIRM_ITEM; + public static final ItemStack CONFIRM_PERMANENT_ITEM; + + static { + CANCEL_ITEM = new ItemStack(Material.RED_TERRACOTTA); + ItemMeta meta = CANCEL_ITEM.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§cCancel"); + meta.setLore(Collections.singletonList("§7Cancel current action and return to previous menu.")); + CANCEL_ITEM.setItemMeta(meta); + + CONFIRM_ITEM = new ItemStack(Material.GREEN_TERRACOTTA); + meta = CONFIRM_ITEM.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§aConfirm"); + meta.setLore(Collections.singletonList("§7Confirm current action.")); + CONFIRM_ITEM.setItemMeta(meta); + + CONFIRM_PERMANENT_ITEM = new ItemStack(Material.GREEN_TERRACOTTA); + meta = CONFIRM_PERMANENT_ITEM.getItemMeta(); + assert meta != null; + + meta.setDisplayName("§aConfirm"); + meta.setLore(Arrays.asList("§7Confirm current action.", + "§4Cation: This action can't be canceled.")); + CONFIRM_PERMANENT_ITEM.setItemMeta(meta); + } + + public static void loadConstants(){} + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/ModrinthUpdateChecker.java b/src/main/java/xyz/alexcrea/cuanvil/update/ModrinthUpdateChecker.java new file mode 100644 index 0000000..489c636 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/ModrinthUpdateChecker.java @@ -0,0 +1,214 @@ +/* + * MIT License + * + * Copyright (c) 2025 Clickism + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package xyz.alexcrea.cuanvil.update; + +import com.google.gson.*; +import org.jetbrains.annotations.Nullable; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Utility class to check for newer versions of a project hosted on Modrinth. + */ +public class ModrinthUpdateChecker { + + private static final String API_URL = "https://api.modrinth.com/v2/project/{id}/version"; + + private final String projectId; + private final String loader; + @Nullable + private final String minecraftVersion; + + @Nullable + private Boolean featured = null; + + @Nullable + public Consumer onError = null; + @Nullable + public Function getRawVersion = ModrinthUpdateChecker::getRawVersion; + + /** + * Create a new update checker for the given project. + * This will check the latest version for the given loader and any minecraft version. + * + * @param projectId the project ID + * @param loader the loader + */ + public ModrinthUpdateChecker(String projectId, String loader) { + this(projectId, loader, null); + } + + /** + * Create a new update checker for the given project. + * This will check the latest version for the given loader and minecraft version. + * + * @param projectId the project ID + * @param loader the loader + * @param minecraftVersion the minecraft version, or null for any version + */ + public ModrinthUpdateChecker(String projectId, String loader, @Nullable String minecraftVersion) { + this.projectId = projectId; + this.loader = loader; + this.minecraftVersion = minecraftVersion; + } + + /** + * Check the latest version of the project for the given loader and minecraft version + * and call the consumer with it. + * + * @param consumer the consumer + */ + public void checkVersion(Consumer consumer) { + try { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(prepareURI()) + .GET() + .build(); + + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenAcceptAsync(response -> { + if (response.statusCode() != 200) { + if(onError != null) + onError.accept(new RuntimeException("wrong response status code: " + response.statusCode())); + return; + } + JsonArray versionsArray = JsonParser.parseString(response.body()).getAsJsonArray(); + String latestVersion = getLatestVersion(versionsArray); + if (latestVersion == null) { + if(onError != null) + onError.accept(new RuntimeException("latest version is null")); + return; + } + consumer.accept(latestVersion); + }); + } catch (Exception e) { + if(onError != null) onError.accept(e); + } + } + + /** + * Get the latest compatible version from the versions array. + * + * @param versions the versions array + * @return the latest compatible version + */ + @Nullable + protected String getLatestVersion(JsonArray versions) { + return versions.asList().stream().findFirst() + .map(JsonElement::getAsJsonObject) + .map(version -> version.get("version_number").getAsString()) + .map(getRawVersion != null ? getRawVersion : (v -> v)) + .orElse(null); + } + + /** + * Gets the raw version from a version string. + * i.E: "fabric-1.2+1.17.1" -> "1.2" + * + * @param version the version string + * @return the raw version string + */ + public static String getRawVersion(String version) { + if (version.isEmpty()) return version; + version = version.replaceAll("^\\D+", ""); + String[] split = version.split("\\+"); + return split[0]; + } + + /** + * Prepare this request uri based on current parameters. + * @return the request uri + */ + private URI prepareURI() { + var url = new StringBuilder(API_URL.replace("{id}", projectId)); + + var parameters = prepareParameters(); + String[] paramArray = new String[parameters.size()]; + int i = 0; + for (Map.Entry entry : parameters.entrySet()) { + paramArray[i++] = entry.getKey() + '=' + entry.getValue(); + } + url.append('?').append(String.join("&", paramArray)); + + return URI.create(url.toString()); + } + + /** + * Get the parameters for the version request. + * + * @return a map of key-value map of the request parameters + */ + private Map prepareParameters(){ + var parameters = new HashMap(); + + parameters.put("loaders", List.of(loader).toString()); + if(minecraftVersion != null) parameters.put("game_versions", List.of(minecraftVersion).toString()); + if(featured != null) parameters.put("featured", featured.toString()); + + parameters.put("include_changelog", "false"); + return parameters; + } + + /** + * Only get featured or non-featured versions. + * Null represent no filter. + * @param featured should be restricted to featured version ? default null if not called + * @return this + */ + public ModrinthUpdateChecker setFeatured(@Nullable Boolean featured) { + this.featured = featured; + return this; + } + + /** + * Function called on error calling the api. + * @param onError What should happen on error + * @return this + */ + public ModrinthUpdateChecker setOnError(@Nullable Consumer onError) { + this.onError = onError; + return this; + } + + /** + * Set the function to get raw version from the modrinth version. + * If null provided raw version will act as in the identity function. + * @param getRawVersion The function transforming modrinth version to raw version + * @return this + */ + public ModrinthUpdateChecker setGetRawVersion(@Nullable Function getRawVersion) { + this.getRawVersion = getRawVersion; + return this; + } +} \ No newline at end of file diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/PluginSetDefault.java b/src/main/java/xyz/alexcrea/cuanvil/update/PluginSetDefault.java new file mode 100644 index 0000000..707a218 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/PluginSetDefault.java @@ -0,0 +1,98 @@ +package xyz.alexcrea.cuanvil.update; + +import io.delilaheve.CustomAnvil; +import io.delilaheve.util.ConfigOptions; +import org.bukkit.configuration.file.FileConfiguration; +import org.jetbrains.annotations.NotNull; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.util.MetricType; +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil; +import xyz.alexcrea.cuanvil.util.config.LoreEditType; + +import static io.delilaheve.util.ConfigOptions.*; +import static xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.*; + +public class PluginSetDefault { + + public static void reAddMissingDefault() { + FileConfiguration config = ConfigHolder.DEFAULT_CONFIG.getConfig(); + + int nbSet = 0; + + nbSet += trySetDefault(config, METRIC_TYPE, MetricType.AUTO.getValue()); + nbSet += trySetDefault(config, METRIC_COLLECT_ERROR, true); + + nbSet += trySetDefault(config, CAP_ANVIL_COST, DEFAULT_CAP_ANVIL_COST); + nbSet += trySetDefault(config, MAX_ANVIL_COST, DEFAULT_MAX_ANVIL_COST); + nbSet += trySetDefault(config, REMOVE_ANVIL_COST_LIMIT, DEFAULT_REMOVE_ANVIL_COST_LIMIT); + nbSet += trySetDefault(config, REPLACE_TOO_EXPENSIVE, DEFAULT_REPLACE_TOO_EXPENSIVE); + nbSet += trySetDefault(config, ITEM_REPAIR_COST, DEFAULT_ITEM_REPAIR_COST); + nbSet += trySetDefault(config, UNIT_REPAIR_COST, DEFAULT_UNIT_REPAIR_COST); + nbSet += trySetDefault(config, ITEM_RENAME_COST, DEFAULT_ITEM_RENAME_COST); + nbSet += trySetDefault(config, SACRIFICE_ILLEGAL_COST, DEFAULT_SACRIFICE_ILLEGAL_COST); + nbSet += trySetDefault(config, ConfigOptions.ALLOW_COLOR_CODE, ConfigOptions.DEFAULT_ALLOW_COLOR_CODE); + nbSet += trySetDefault(config, ALLOW_HEXADECIMAL_COLOR, DEFAULT_ALLOW_HEXADECIMAL_COLOR); + nbSet += trySetDefault(config, PERMISSION_NEEDED_FOR_COLOR, DEFAULT_PERMISSION_NEEDED_FOR_COLOR); + nbSet += trySetDefault(config, USE_OF_COLOR_COST, DEFAULT_USE_OF_COLOR_COST); + nbSet += trySetDefault(config, PER_COLOR_CODE_PERMISSION, DEFAULT_PER_COLOR_CODE_PERMISSION); + + // Lore Edit defaults + for (@NotNull LoreEditType value : LoreEditType.values()) { + String path = value.getRootPath() + "."; + + nbSet += trySetDefault(config, path + IS_ENABLED, DEFAULT_IS_ENABLED); + nbSet += trySetDefault(config, path + FIXED_COST, DEFAULT_FIXED_COST); + + nbSet += trySetDefault(config, path + DO_CONSUME, DEFAULT_DO_CONSUME); + if (value.isMultiLine()) { + nbSet += trySetDefault(config, path + PER_LINE_COST, DEFAULT_PER_LINE_COST); + } + if (value.isAppend()) { + nbSet += trySetDefault(config, path + LoreEditConfigUtil.ALLOW_COLOR_CODE, LoreEditConfigUtil.DEFAULT_ALLOW_COLOR_CODE); + nbSet += trySetDefault(config, path + ALLOW_HEX_COLOR, DEFAULT_ALLOW_HEX_COLOR); + nbSet += trySetDefault(config, path + USE_COLOR_COST, DEFAULT_USE_COLOR_COST); + } else { + nbSet += trySetDefault(config, path + REMOVE_COLOR_COST, DEFAULT_REMOVE_COLOR_COST); + } + } + + nbSet += trySetDefault(config, BOOK_PERMISSION_NEEDED, DEFAULT_BOOK_PERMISSION_NEEDED); + nbSet += trySetDefault(config, PAPER_PERMISSION_NEEDED, DEFAULT_PAPER_PERMISSION_NEEDED); + + nbSet += trySetDefault(config, PAPER_EDIT_ORDER, DEFAULT_PAPER_EDIT_ORDER); + + nbSet += trySetDefault(config, DIALOG_RENAME_ENABLED, DEFAULT_DIALOG_RENAME_ENABLED); + nbSet += trySetDefault(config, DIALOG_MAX_SIZE, DEFAULT_DIALOG_MAX_SIZE); + nbSet += trySetDefault(config, DIALOG_RENAME_USE_PERMISSION, DEFAULT_DIALOG_RENAME_USE_PERMISSION); + nbSet += trySetDefault(config, DIALOG_KEEP_USER_TEXT, DEFAULT_DIALOG_KEEP_USER_TEXT); + + if (nbSet > 0) { + CustomAnvil.instance.getLogger().info("Adding " + nbSet + " absent default config values."); + ConfigHolder.DEFAULT_CONFIG.saveToDisk(true); + } + + } + + private static int trySetDefault(@NotNull FileConfiguration config, @NotNull String path, @NotNull String value) { + if (config.isSet(path)) return 0; + + config.set(path, value); + return 1; + } + + private static int trySetDefault(@NotNull FileConfiguration config, @NotNull String path, int value) { + if (config.isSet(path)) return 0; + + config.set(path, value); + return 1; + } + + private static int trySetDefault(@NotNull FileConfiguration config, @NotNull String path, boolean value) { + if (config.isSet(path)) return 0; + + config.set(path, value); + return 1; + } + + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/UpdateHandler.java b/src/main/java/xyz/alexcrea/cuanvil/update/UpdateHandler.java new file mode 100644 index 0000000..660accb --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/UpdateHandler.java @@ -0,0 +1,105 @@ +package xyz.alexcrea.cuanvil.update; + +import io.delilaheve.CustomAnvil; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.update.minecraft.MCUpdate; +import xyz.alexcrea.cuanvil.update.minecraft.Update_1_21; +import xyz.alexcrea.cuanvil.update.minecraft.Update_1_21_11; +import xyz.alexcrea.cuanvil.update.minecraft.Update_1_21_9; +import xyz.alexcrea.cuanvil.update.plugin.*; + +import javax.annotation.Nonnull; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +public class UpdateHandler { + + private static final String CONFIG_VERSION_PATH = "configVersion"; + + // Handle mc version update then plugin version update + public static void handleUpdates() { + handleMCVersionUpdate(); + handlePluginUpdate(); + } + + private static final Map>> pUpdateMap = Map.of( + new Version(1, 6, 2), PUpdate_1_6_2::handleUpdate, + new Version(1, 6, 7), PUpdate_1_6_7::handleUpdate, + new Version(1, 8, 0), PUpdate_1_8_0::handleUpdate, + new Version(1, 11, 0), PUpdate_1_11_0::handleUpdate, + new Version(1, 15, 5), PUpdate_1_15_5::handleUpdate, + new Version(1, 15, 6), PUpdate_1_15_6::handleUpdate + ); + + private static final List mcUpdateMap = List.of( + new Update_1_21(), + new Update_1_21_9(), + new Update_1_21_11() + ); + + // Handle only plugin update + private static void handlePluginUpdate() { + String versionString = ConfigHolder.DEFAULT_CONFIG.getConfig().getString(CONFIG_VERSION_PATH); + Version current = versionString == null ? new Version(0) : Version.fromString(versionString); + + Set toSave = new HashSet<>(); + + AtomicReference latest = new AtomicReference<>(null); + + // Hopefully, should iterate in the "insertion" order + pUpdateMap.forEach((ver, consumer) -> { + if (ver.greaterThan(current)) { + CustomAnvil.log("handling plugin update to " + ver); + consumer.accept(toSave); + + latest.set(ver); + } + }); + + if (latest.get() != null) { + finishConfiguration(latest.get().toString(), toSave); + } + } + + // Handle minecraft version update (not plugin version update) + public static void handleMCVersionUpdate() { + Version current = UpdateUtils.currentMinecraftVersion(); + + boolean hadUpdate = false; + for (MCUpdate mcUpdate : mcUpdateMap) { + hadUpdate |= mcUpdate.handleUpdate(current, hadUpdate); + } + + if (hadUpdate) { + CustomAnvil.instance.getLogger().info("Updating Done !"); + } + + if(current.major() == 1 && current.minor() < 21) { + var logger = CustomAnvil.instance.getLogger(); + logger.warning("Your are running an old version of minecraft (lower than 1.21)"); + logger.warning("Custom Anvil will stop supporting this version on the first of july 2026"); + } + } + + private static void finishConfiguration(@Nonnull String newVersion, @Nonnull Set toSave) { + CustomAnvil.instance.getLogger().info("Configuration file updated to " + newVersion); + ConfigHolder.DEFAULT_CONFIG.getConfig().set(CONFIG_VERSION_PATH, newVersion); + + toSave.add(ConfigHolder.DEFAULT_CONFIG); + // save + for (ConfigHolder configHolder : toSave) { + configHolder.saveToDisk(true); + } + + // then reload + for (ConfigHolder configHolder : toSave) { + configHolder.reload(); + } + + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/UpdateUtils.java b/src/main/java/xyz/alexcrea/cuanvil/update/UpdateUtils.java new file mode 100644 index 0000000..2907fef --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/UpdateUtils.java @@ -0,0 +1,36 @@ +package xyz.alexcrea.cuanvil.update; + +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.FileConfiguration; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class UpdateUtils { + public static final String MINECRAFT_VERSION_PATH = "lowMinecraftVersion"; + + public static Version currentMinecraftVersion() { + String versionString = Bukkit.getServer().getBukkitVersion().split("-")[0]; + return Version.fromString(versionString); + } + + public static void addToStringList(FileConfiguration config, String path, String... toAdd) { + List groups = new ArrayList<>(config.getStringList(path)); + groups.addAll(Arrays.asList(toAdd)); + config.set(path, groups); + + } + + public static void addAbsentToList(FileConfiguration config, String path, String... toAdd) { + List groups = new ArrayList<>(config.getStringList(path)); + for (String val : toAdd) { + if (groups.contains(val)) continue; + + groups.add(val); + } + config.set(path, groups); + + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/Version.java b/src/main/java/xyz/alexcrea/cuanvil/update/Version.java new file mode 100644 index 0000000..a49fbdd --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/Version.java @@ -0,0 +1,62 @@ +package xyz.alexcrea.cuanvil.update; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public record Version(int major, int minor, int patch) { + + public Version(int major, int minor){ + this(major, minor, 0); + } + public Version(int major){ + this(major, 0, 0); + } + + public static Version fromString(@Nullable String versionString){ + if(versionString == null) return new Version(0, 0, 0); + + String[] partialVersion = versionString.split("\\."); + int[] versionParts = new int[]{0, 0, 0}; + + for (int i = 0; i < Math.min(3, partialVersion.length); i++) { + try { + versionParts[i] = Integer.parseInt(partialVersion[i]); + } catch (NumberFormatException e) { + break; + } + } + return new Version(versionParts[0], versionParts[1], versionParts[2]); + } + + public boolean greaterThan(@Nonnull Version other){ + return this.major > other.major || (this.major == other.major && + (this.minor > other.minor || (this.minor == other.minor && + this.patch > other.patch))); + } + + public boolean greaterEqual(@Nonnull Version other){ + return this.major > other.major || (this.major == other.major && + (this.minor > other.minor || (this.minor == other.minor && + this.patch >= other.patch))); + } + + public boolean lesserThan(@Nonnull Version other){ + return this.major < other.major || (this.major == other.major && + (this.minor < other.minor || (this.minor == other.minor && + this.patch < other.patch))); + } + + public boolean lesserEqual(@Nonnull Version other){ + return this.major < other.major || (this.major == other.major && + (this.minor < other.minor || (this.minor == other.minor && + this.patch <= other.patch))); + } + + @NotNull + @Override + public String toString() { + return major + "." + minor + "." + patch; + } +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/minecraft/MCUpdate.java b/src/main/java/xyz/alexcrea/cuanvil/update/minecraft/MCUpdate.java new file mode 100644 index 0000000..40fc587 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/minecraft/MCUpdate.java @@ -0,0 +1,38 @@ +package xyz.alexcrea.cuanvil.update.minecraft; + +import io.delilaheve.CustomAnvil; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.update.UpdateUtils; +import xyz.alexcrea.cuanvil.update.Version; + +public abstract class MCUpdate { + + public final Version version; + + public MCUpdate(Version version){ + this.version = version; + } + + public boolean handleUpdate(Version current, boolean hadUpdate){ + // Test if we are running in this update version or better + if(version.greaterThan(current)) + return false; + + // if version path is not null then check if its it's before this update version + String oldVersion = ConfigHolder.DEFAULT_CONFIG.getConfig().getString(UpdateUtils.MINECRAFT_VERSION_PATH); + if(oldVersion != null){ + var version = Version.fromString(oldVersion); + if(this.version.lesserEqual(version)) return false; + } + + if(!hadUpdate){ + CustomAnvil.instance.getLogger().info("Updating config to support minecraft " + current +" ..."); + } + doUpdate(); + return true; + } + + protected abstract void doUpdate(); + + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/minecraft/Update_1_21.java b/src/main/java/xyz/alexcrea/cuanvil/update/minecraft/Update_1_21.java new file mode 100644 index 0000000..3aa6073 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/minecraft/Update_1_21.java @@ -0,0 +1,79 @@ +package xyz.alexcrea.cuanvil.update.minecraft; + +import io.delilaheve.CustomAnvil; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.update.UpdateUtils; +import xyz.alexcrea.cuanvil.update.Version; + +import static xyz.alexcrea.cuanvil.update.UpdateUtils.addAbsentToList; + +public class Update_1_21 extends MCUpdate { + + public Update_1_21() { + super(new Version(1, 21)); + } + + @Override + protected void doUpdate() { + var baseConfig = ConfigHolder.DEFAULT_CONFIG.getConfig(); + var groupConfig = ConfigHolder.ITEM_GROUP_HOLDER.getConfig(); + var conflictConfig = ConfigHolder.CONFLICT_HOLDER.getConfig(); + var unitConfig = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig(); + + // Add mace to groups + groupConfig.set("mace.type", "include"); + addAbsentToList(groupConfig, "mace.items", "mace"); + + addAbsentToList(groupConfig, "can_unbreak.groups", "mace"); + + // Add new enchant conflicts + addAbsentToList(conflictConfig, "restriction_density.enchantments", "minecraft:density"); + addAbsentToList(conflictConfig, "restriction_density.notAffectedGroups", "mace", "enchanted_book"); + + addAbsentToList(conflictConfig, "restriction_breach.enchantments", "minecraft:breach"); + addAbsentToList(conflictConfig, "restriction_breach.notAffectedGroups", "mace", "enchanted_book"); + + addAbsentToList(conflictConfig, "restriction_wind_burst.enchantments", "minecraft:wind_burst"); + addAbsentToList(conflictConfig, "restriction_wind_burst.notAffectedGroups", "mace", "enchanted_book"); + + // Add mace to conflicts + addAbsentToList(conflictConfig, "restriction_fire_aspect.notAffectedGroups", "mace"); + addAbsentToList(conflictConfig, "restriction_smite.notAffectedGroups", "mace"); + addAbsentToList(conflictConfig, "restriction_bane_of_arthropods.notAffectedGroups", "mace"); + + addAbsentToList(conflictConfig, "sword_enchant_conflict.enchantments", + "minecraft:density", "minecraft:breach"); + + // Add level limit + baseConfig.set("enchant_limits.minecraft:density", 5); + baseConfig.set("enchant_limits.minecraft:breach", 4); + baseConfig.set("enchant_limits.minecraft:wind_burst", 3); + + // Add enchant values + baseConfig.set("enchant_values.minecraft:density.item", 2); + baseConfig.set("enchant_values.minecraft:density.book", 1); + + baseConfig.set("enchant_values.minecraft:breach.item", 4); + baseConfig.set("enchant_values.minecraft:breach.book", 2); + + baseConfig.set("enchant_values.minecraft:wind_burst.item", 4); + baseConfig.set("enchant_values.minecraft:wind_burst.book", 2); + + // Add unit repair for mace + unitConfig.set("breeze_rod.mace", 0.25); + + // Set version string as current + baseConfig.set(UpdateUtils.MINECRAFT_VERSION_PATH, version.toString()); + + // Save + ConfigHolder.DEFAULT_CONFIG.saveToDisk(true); + ConfigHolder.ITEM_GROUP_HOLDER.saveToDisk(true); + ConfigHolder.CONFLICT_HOLDER.saveToDisk(true); + ConfigHolder.UNIT_REPAIR_HOLDER.saveToDisk(true); + + // imply reload of CONFLICT_HOLDER + // We also do not need to reload base config as there is no object related to it. + ConfigHolder.ITEM_GROUP_HOLDER.reload(); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/minecraft/Update_1_21_11.java b/src/main/java/xyz/alexcrea/cuanvil/update/minecraft/Update_1_21_11.java new file mode 100644 index 0000000..d639596 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/minecraft/Update_1_21_11.java @@ -0,0 +1,84 @@ +package xyz.alexcrea.cuanvil.update.minecraft; + +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.update.UpdateUtils; +import xyz.alexcrea.cuanvil.update.Version; + +import static xyz.alexcrea.cuanvil.update.UpdateUtils.addAbsentToList; + +public class Update_1_21_11 extends MCUpdate{ + + public Update_1_21_11() { + super(new Version(1, 21, 11)); + } + + @Override + protected void doUpdate() { + var baseConfig = ConfigHolder.DEFAULT_CONFIG.getConfig(); + var groupConfig = ConfigHolder.ITEM_GROUP_HOLDER.getConfig(); + var conflictConfig = ConfigHolder.CONFLICT_HOLDER.getConfig(); + var unitConfig = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig(); + + // Create spear group + groupConfig.set("spears.type", "include"); + addAbsentToList(groupConfig, "spears.items", + "wooden_spear", + "golden_spear", + "stone_spear", + "copper_spear", + "iron_spear", + "diamond_spear", + "netherite_spear"); + + // Add spear group to super group and enchantments + addAbsentToList(groupConfig, "melee_weapons.groups", "spears"); + + addAbsentToList(conflictConfig, "restriction_looting.notAffectedGroups", "spears"); + addAbsentToList(conflictConfig, "restriction_knockback.notAffectedGroups", "spears"); + addAbsentToList(conflictConfig, "restriction_fire_aspect.notAffectedGroups", "spears"); + + // Unit repair for spears + unitConfig.set("gold_ingot.golden_spear", 0.25); + unitConfig.set("copper_ingot.copper_spear", 0.25); + unitConfig.set("iron_ingot.iron_spear", 0.25); + unitConfig.set("diamond.diamond_spear", 0.25); + unitConfig.set("netherite_ingot.netherite_spear", 0.25); + + unitConfig.set("cobblestone.stone_spear", 0.25); + unitConfig.set("cobbled_deepslate.stone_spear", 0.25); + + unitConfig.set("oak_planks.wooden_spear", 0.25); + unitConfig.set("spruce_planks.wooden_spear", 0.25); + unitConfig.set("birch_planks.wooden_spear", 0.25); + unitConfig.set("jungle_planks.wooden_spear", 0.25); + unitConfig.set("acacia_planks.wooden_spear", 0.25); + unitConfig.set("dark_oak_planks.wooden_spear", 0.25); + unitConfig.set("mangrove_planks.wooden_spear", 0.25); + unitConfig.set("cherry_planks.wooden_spear", 0.25); + unitConfig.set("bamboo_planks.wooden_spear", 0.25); + unitConfig.set("crimson_planks.wooden_spear", 0.25); + unitConfig.set("warped_planks.wooden_spear", 0.25); + + // Create lunge enchant value and group + baseConfig.set("enchant_limits.minecraft:lunge", 3); + baseConfig.set("enchant_values.minecraft:lunge.item", 2); + baseConfig.set("enchant_values.minecraft:lunge.book", 1); + + addAbsentToList(conflictConfig, "restriction_lunge.enchantments", "minecraft:lunge"); + addAbsentToList(conflictConfig, "restriction_lunge.notAffectedGroups", "spears", "enchanted_book"); + + // Set version string as current + baseConfig.set(UpdateUtils.MINECRAFT_VERSION_PATH, version.toString()); + + // Save + ConfigHolder.DEFAULT_CONFIG.saveToDisk(true); + ConfigHolder.ITEM_GROUP_HOLDER.saveToDisk(true); + ConfigHolder.CONFLICT_HOLDER.saveToDisk(true); + ConfigHolder.UNIT_REPAIR_HOLDER.saveToDisk(true); + + // imply reload of CONFLICT_HOLDER + // We also do not need to reload base config as there is no object related to it. + ConfigHolder.ITEM_GROUP_HOLDER.reload(); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/minecraft/Update_1_21_9.java b/src/main/java/xyz/alexcrea/cuanvil/update/minecraft/Update_1_21_9.java new file mode 100644 index 0000000..d289b9b --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/minecraft/Update_1_21_9.java @@ -0,0 +1,64 @@ +package xyz.alexcrea.cuanvil.update.minecraft; + +import org.bukkit.configuration.file.FileConfiguration; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.update.UpdateUtils; +import xyz.alexcrea.cuanvil.update.Version; + +import static xyz.alexcrea.cuanvil.update.UpdateUtils.addAbsentToList; + +public class Update_1_21_9 extends MCUpdate{ + + public Update_1_21_9() { + super(new Version(1, 21, 9)); + } + + @Override + protected void doUpdate() { + var baseConfig = ConfigHolder.DEFAULT_CONFIG.getConfig(); + var groupConfig = ConfigHolder.ITEM_GROUP_HOLDER.getConfig(); + var unitConfig = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig(); + + // Add cooper items to groups + addAbsentToList(groupConfig, "helmets.items", "copper_helmet"); + addAbsentToList(groupConfig, "chestplate.items", "copper_chestplate"); + addAbsentToList(groupConfig, "leggings.items", "copper_leggings"); + addAbsentToList(groupConfig, "boots.items", "copper_boots"); + + addAbsentToList(groupConfig, "pickaxes.items", "copper_pickaxe"); + addAbsentToList(groupConfig, "shovels.items", "copper_shovel"); + addAbsentToList(groupConfig, "hoes.items", "copper_hoe"); + addAbsentToList(groupConfig, "axes.items", "copper_axe"); + addAbsentToList(groupConfig, "swords.items", "copper_sword"); + + // Add unit repair + addCopperUnitRepair(unitConfig); + + // Set version string as current + baseConfig.set(UpdateUtils.MINECRAFT_VERSION_PATH, version.toString()); + + // Save + ConfigHolder.DEFAULT_CONFIG.saveToDisk(true); + ConfigHolder.ITEM_GROUP_HOLDER.saveToDisk(true); + ConfigHolder.UNIT_REPAIR_HOLDER.saveToDisk(true); + + // imply reload of CONFLICT_HOLDER + // We also do not need to reload base config as there is no object related to it. + ConfigHolder.ITEM_GROUP_HOLDER.reload(); + } + + public static void addCopperUnitRepair(FileConfiguration unitConfig) { + // Add unit repair + unitConfig.set("copper_ingot.copper_helmet", 0.25); + unitConfig.set("copper_ingot.copper_chestplate", 0.25); + unitConfig.set("copper_ingot.copper_leggings", 0.25); + unitConfig.set("copper_ingot.copper_boots", 0.25); + + unitConfig.set("copper_ingot.copper_pickaxe", 0.25); + unitConfig.set("copper_ingot.copper_shovel", 0.25); + unitConfig.set("copper_ingot.copper_hoe", 0.25); + unitConfig.set("copper_ingot.copper_axe", 0.25); + unitConfig.set("copper_ingot.copper_sword", 0.25); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_11_0.java b/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_11_0.java new file mode 100644 index 0000000..9740971 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_11_0.java @@ -0,0 +1,137 @@ +package xyz.alexcrea.cuanvil.update.plugin; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.api.MaterialGroupApi; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.group.AbstractMaterialGroup; +import xyz.alexcrea.cuanvil.group.IncludeGroup; + +import javax.annotation.Nonnull; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static xyz.alexcrea.cuanvil.update.UpdateUtils.addAbsentToList; + +public class PUpdate_1_11_0 { + + private static final List mace_expected = List.of( + "density", + "breach", + "smite", + "bane_of_arthropods" + ); + private static final List sword_expected = List.of( + "sharpness", + "smite", + "bane_of_arthropods" + ); + + private static final Material[] PICKAXES = new Material[]{ + Material.WOODEN_PICKAXE, Material.STONE_PICKAXE, + Material.IRON_PICKAXE, Material.DIAMOND_PICKAXE, + Material.GOLDEN_PICKAXE, Material.NETHERITE_PICKAXE + }; + + private static final Material[] SHOVELS = new Material[]{ + Material.WOODEN_SHOVEL, Material.STONE_SHOVEL, + Material.IRON_SHOVEL, Material.DIAMOND_SHOVEL, + Material.GOLDEN_SHOVEL, Material.NETHERITE_SHOVEL + }; + + private static final Material[] HOES = new Material[]{ + Material.WOODEN_HOE, Material.STONE_HOE, + Material.IRON_HOE, Material.DIAMOND_HOE, + Material.GOLDEN_HOE, Material.NETHERITE_HOE + }; + + public static void handleUpdate(@Nonnull Set toSave) { + handleToolsMigration(); + handleMaceMigration(toSave); + } + + private static void handleToolsMigration() { + // We migrate the mace conflict if exist and unmodified + AbstractMaterialGroup tools = MaterialGroupApi.getGroup("tools"); + + migrateTools(tools, "pickaxes", PICKAXES); + migrateTools(tools, "shovels", SHOVELS); + migrateTools(tools, "hoes", HOES); + } + + private static void migrateTools( + @Nullable AbstractMaterialGroup tools, + @NotNull String toolset, + @NotNull Material[] toolMats) { + + // Create new group + IncludeGroup group = new IncludeGroup(toolset); + NamespacedKey[] keys = new NamespacedKey[toolMats.length]; + for (int i = 0; i < toolMats.length; i++) { + keys[i] = toolMats[i].getKey(); + } + + group.addAll(keys); + + MaterialGroupApi.addMaterialGroup(group, true); + + // Try to see if all the materials was in the tools group. and if so, replace it with the new group + if (tools == null) return; + if (!(tools instanceof IncludeGroup include)) return; + + List mats = List.of(keys); + Set matSet = include.getNonGroupInheritedMaterials(); + if (!matSet.containsAll(mats)) return; + + mats.forEach(matSet::remove); + tools.addToPolicy(group); + MaterialGroupApi.writeMaterialGroup(tools); + } + + private static void handleMaceMigration(@Nonnull Set toSave) { + // We migrate the mace conflict if exist and unmodified + FileConfiguration config = ConfigHolder.CONFLICT_HOLDER.getConfig(); + + if (!config.isConfigurationSection("sword_enchant_conflict")) return; + if (!config.isConfigurationSection("mace_enchant_conflict")) return; + + ConfigurationSection mace_conflict = config.getConfigurationSection("mace_enchant_conflict"); + // Test mace conflict if default + if (mace_conflict == null) return; + if (mace_conflict.getInt("maxEnchantmentBeforeConflict", 0) != 1) return; + + if (mace_conflict.isList("notAffectedGroups") && !mace_conflict.getList("notAffectedGroups").isEmpty()) return; + + List enchantments = mace_conflict.getStringList("enchantments"); + if (enchantments.size() != 4) return; + for (String ench : mace_expected) { + if (!enchantments.contains(ench) && !enchantments.contains("minecraft:" + ench)) return; + } + + // Test sword_enchant_conflict is default + ConfigurationSection sword_conflict = config.getConfigurationSection("sword_enchant_conflict"); + if (sword_conflict.getInt("maxEnchantmentBeforeConflict", 0) != 1) return; + + if (sword_conflict.isList("notAffectedGroups") && !sword_conflict.getList("notAffectedGroups").isEmpty()) + return; + + enchantments = sword_conflict.getStringList("enchantments"); + if (enchantments.size() != 3) return; + for (String ench : sword_expected) { + if (!enchantments.contains(ench) && !enchantments.contains("minecraft:" + ench)) return; + } + + // Finally we know both conflict are default. so we fix + addAbsentToList(config, "sword_enchant_conflict.enchantments", + "minecraft:density", "minecraft:breach"); + + config.set("mace_enchant_conflict", null); + toSave.add(ConfigHolder.CONFLICT_HOLDER); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_15_5.java b/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_15_5.java new file mode 100644 index 0000000..76f51af --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_15_5.java @@ -0,0 +1,27 @@ +package xyz.alexcrea.cuanvil.update.plugin; + +import org.bukkit.configuration.file.FileConfiguration; +import xyz.alexcrea.cuanvil.config.ConfigHolder; + +import javax.annotation.Nonnull; +import java.util.Set; + +import static xyz.alexcrea.cuanvil.update.UpdateUtils.addAbsentToList; + +public class PUpdate_1_15_5 { + + public static void handleUpdate(@Nonnull Set toSave) { + FileConfiguration config = ConfigHolder.CONFLICT_HOLDER.getConfig(); + + if (config.isConfigurationSection("restriction_luck_of_the_sea")) return; + + // We fix the luck of the see enchantment + addAbsentToList(config, "restriction_luck_of_the_sea.enchantments", + "minecraft:luck_of_the_sea"); + addAbsentToList(config, "restriction_luck_of_the_sea.notAffectedGroups", + "enchanted_book", "fishing_rod"); + + toSave.add(ConfigHolder.CONFLICT_HOLDER); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_15_6.java b/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_15_6.java new file mode 100644 index 0000000..fde7cfc --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_15_6.java @@ -0,0 +1,27 @@ +package xyz.alexcrea.cuanvil.update.plugin; + +import org.bukkit.configuration.file.FileConfiguration; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.update.UpdateUtils; +import xyz.alexcrea.cuanvil.update.Version; +import xyz.alexcrea.cuanvil.update.minecraft.Update_1_21_9; + +import javax.annotation.Nonnull; +import java.util.Set; + +public class PUpdate_1_15_6 { + + public static void handleUpdate(@Nonnull Set toSave) { + // fix only needed for 1.21.9 and above + Version current = UpdateUtils.currentMinecraftVersion(); + if (new Version(1, 21, 9).greaterThan(current)) return; + + FileConfiguration unitConfig = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig(); + + // Add unit repair + Update_1_21_9.addCopperUnitRepair(unitConfig); + + toSave.add(ConfigHolder.UNIT_REPAIR_HOLDER); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_6_2.java b/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_6_2.java new file mode 100644 index 0000000..a4078de --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_6_2.java @@ -0,0 +1,55 @@ +package xyz.alexcrea.cuanvil.update.plugin; + +import org.bukkit.configuration.file.FileConfiguration; +import xyz.alexcrea.cuanvil.config.ConfigHolder; + +import javax.annotation.Nonnull; +import java.util.Set; + +import static xyz.alexcrea.cuanvil.update.UpdateUtils.addAbsentToList; + +public class PUpdate_1_6_2 { + + private static final String[] toUpdate = new String[] {"restriction_density", "restriction_breach", "restriction_wind_burst"}; + + public static void handleUpdate(@Nonnull Set toSave) { + FileConfiguration config = ConfigHolder.CONFLICT_HOLDER.getConfig(); + + boolean conflictUpdated = false; + for (String restriction : toUpdate) { + if(!config.isConfigurationSection(restriction)) continue; + String path = restriction + ".notAffectedGroups"; + + boolean contained = false; + for (String value : config.getStringList(path)) { + if(value.equalsIgnoreCase("enchanted_book")) { + contained = true; + break; + } + } + + if(!contained){ + addAbsentToList(config, path, "enchanted_book"); + conflictUpdated = true; + } + } + + if(conflictUpdated){ + toSave.add(ConfigHolder.CONFLICT_HOLDER); + + // May not be the most efficient for later revision, maybe move to PluginUpdates + ConfigHolder.CONFLICT_HOLDER.reload(); + } + + // Then we add the unit repair + config = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig(); + String unitRepairPath = "breeze_rod.mace"; + if(!config.isConfigurationSection(unitRepairPath)){ + config.set(unitRepairPath, 0.25); + + toSave.add(ConfigHolder.UNIT_REPAIR_HOLDER); + } + + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_6_7.java b/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_6_7.java new file mode 100644 index 0000000..ee544dc --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_6_7.java @@ -0,0 +1,25 @@ +package xyz.alexcrea.cuanvil.update.plugin; + +import org.bukkit.configuration.file.FileConfiguration; +import xyz.alexcrea.cuanvil.config.ConfigHolder; + +import javax.annotation.Nonnull; +import java.util.Set; + +public class PUpdate_1_6_7 { + + public static void handleUpdate(@Nonnull Set toSave) { + FileConfiguration config = ConfigHolder.DEFAULT_CONFIG.getConfig(); + + // We fix the density enchantment + String value = config.getString("enchant_values.minecraft:density.item"); + if(value == null) value = config.getString("enchant_values.density.item"); + + if(value == null || "1".equalsIgnoreCase(value)){ + config.set("enchant_values.minecraft:density.item", 2); + + toSave.add(ConfigHolder.DEFAULT_CONFIG); + } + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_8_0.java b/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_8_0.java new file mode 100644 index 0000000..81ce1aa --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/plugin/PUpdate_1_8_0.java @@ -0,0 +1,66 @@ +package xyz.alexcrea.cuanvil.update.plugin; + +import io.delilaheve.util.ConfigOptions; +import org.bukkit.configuration.file.FileConfiguration; +import xyz.alexcrea.cuanvil.anvil.AnvilUseType; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.config.WorkPenaltyType; +import xyz.alexcrea.cuanvil.gui.config.settings.WorkPenaltyTypeSettingGui; + +import javax.annotation.Nonnull; +import java.util.EnumMap; +import java.util.Set; + +public class PUpdate_1_8_0 { + + private static final String WORK_PENALTY_TYPE = "work_penalty_type"; + + public static void handleUpdate(@Nonnull Set toSave) { + FileConfiguration config = ConfigHolder.DEFAULT_CONFIG.getConfig(); + + // We migrate the work penalty type if it exists + String penaltyTypeValue = config.getString(WORK_PENALTY_TYPE); + if (penaltyTypeValue == null) return; + + EnumMap partEnum; + partEnum = new EnumMap<>(ConfigOptions.INSTANCE.getWorkPenaltyType().getPartMap()); + + boolean keepIncrease; + boolean keepAdditive; + + switch (penaltyTypeValue.toLowerCase()) { + case "add_only": + keepIncrease = false; + keepAdditive = true; + break; + case "increase_only": + keepIncrease = true; + keepAdditive = false; + break; + case "disabled": + keepIncrease = false; + keepAdditive = false; + break; + default: + keepIncrease = true; + keepAdditive = true; + } + + for (AnvilUseType type : partEnum.keySet()) { + WorkPenaltyType.WorkPenaltyPart part = partEnum.get(type); + part = new WorkPenaltyType.WorkPenaltyPart( + keepIncrease & part.penaltyIncrease(), + keepAdditive & part.penaltyAdditive(), + part.exclusivePenaltyIncrease(), + part.exclusivePenaltyAdditive()); + partEnum.replace(type, part); + } + + if(WorkPenaltyTypeSettingGui.saveWorkPenalty(partEnum)){ + config.set(WORK_PENALTY_TYPE, null); + } + + toSave.add(ConfigHolder.DEFAULT_CONFIG); + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/util/CasedStringUtil.java b/src/main/java/xyz/alexcrea/cuanvil/util/CasedStringUtil.java new file mode 100644 index 0000000..cb45d1c --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/util/CasedStringUtil.java @@ -0,0 +1,58 @@ +package xyz.alexcrea.cuanvil.util; + +/** + * An incomplete cased string util + */ +public class CasedStringUtil { + + /** + * Transform a snake cased string to an upper-cased spaced string. + *

    + * for example: if we use "hello_world" as an input this function will return "Hello World". + * + * @param snake_cased_string The input string. + * This argument NEED to be a snake cased string, or it will not work + * @return The input as an upper-cased string with space separator. + */ + public static String snakeToUpperSpacedCase(String snake_cased_string) { + if (snake_cased_string.contentEquals("")) return ""; + StringBuilder result = new StringBuilder(); + + for (String word : snake_cased_string.split("_")) { + result.append(" "); + if (word.isEmpty()) continue; + char firstChar = word.charAt(0); + + result.append(Character.toUpperCase(firstChar)); + result.append(word.substring(1)); + } + return result.substring(1); + } + + public static String camelCaseToUpperSpaceCase(String camelCasedString) { + if (camelCasedString.isEmpty()) return camelCasedString; + StringBuilder stb = new StringBuilder(); + + char[] chars = camelCasedString.toCharArray(); + stb.append(chars[0]); + for (int i = 1; i < chars.length; i++) { + char chr = chars[i]; + if (Character.isUpperCase(chr)) { + stb.append(" "); + } + stb.append(chr); + } + + return stb.toString(); + } + + public static String detectToUpperSpacedCase(String toDetect) { + //not advanced detection + if (toDetect.contains("_")) { + return snakeToUpperSpacedCase(toDetect); + } else { + return camelCaseToUpperSpaceCase(toDetect); + } + } + +} diff --git a/src/main/java/xyz/alexcrea/cuanvil/util/LazyValue.java b/src/main/java/xyz/alexcrea/cuanvil/util/LazyValue.java new file mode 100644 index 0000000..3ae8fdd --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/util/LazyValue.java @@ -0,0 +1,36 @@ +package xyz.alexcrea.cuanvil.util; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +public class LazyValue { + + private final Supplier valueSupplier; + private T storedValue; + + public LazyValue(Supplier valueSupplier) { + this.valueSupplier = valueSupplier; + this.storedValue = null; + } + + @Nullable + public T getStored(){ + return storedValue; + } + + @NotNull + public T get(){ + if (storedValue != null) return storedValue; + + synchronized(this) { + if(storedValue == null) { + storedValue = valueSupplier.get(); + } + } + + return storedValue; + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/group/util/Metrics.java b/src/main/java/xyz/alexcrea/cuanvil/util/Metrics.java similarity index 98% rename from src/main/kotlin/xyz/alexcrea/group/util/Metrics.java rename to src/main/java/xyz/alexcrea/cuanvil/util/Metrics.java index da923c2..f21a15c 100644 --- a/src/main/kotlin/xyz/alexcrea/group/util/Metrics.java +++ b/src/main/java/xyz/alexcrea/cuanvil/util/Metrics.java @@ -12,24 +12,20 @@ * * Violations will result in a ban of your plugin and account from bStats. */ -package xyz.alexcrea.group.util; +package xyz.alexcrea.cuanvil.util; -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +import javax.net.ssl.HttpsURLConnection; +import java.io.*; import java.lang.reflect.Method; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; @@ -40,12 +36,6 @@ import java.util.function.Supplier; import java.util.logging.Level; import java.util.stream.Collectors; import java.util.zip.GZIPOutputStream; -import javax.net.ssl.HttpsURLConnection; -import org.bukkit.Bukkit; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.java.JavaPlugin; public class Metrics { diff --git a/src/main/kotlin/io/delilaheve/AnvilEventListener.kt b/src/main/kotlin/io/delilaheve/AnvilEventListener.kt deleted file mode 100644 index bb24bbe..0000000 --- a/src/main/kotlin/io/delilaheve/AnvilEventListener.kt +++ /dev/null @@ -1,171 +0,0 @@ -package io.delilaheve - -import io.delilaheve.util.ConfigOptions -import io.delilaheve.util.EnchantmentUtil.combineWith -import io.delilaheve.util.EnchantmentUtil.enchantmentName -import io.delilaheve.util.ItemUtil.canMergeWith -import io.delilaheve.util.ItemUtil.findEnchantments -import io.delilaheve.util.ItemUtil.isBook -import io.delilaheve.util.ItemUtil.repairFrom -import io.delilaheve.util.ItemUtil.setEnchantmentsUnsafe -import org.bukkit.event.Event -import org.bukkit.event.EventHandler -import org.bukkit.event.EventPriority.HIGHEST -import org.bukkit.event.Listener -import org.bukkit.event.inventory.InventoryClickEvent -import org.bukkit.event.inventory.PrepareAnvilEvent -import org.bukkit.inventory.AnvilInventory -import org.bukkit.inventory.InventoryView.Property.REPAIR_COST -import org.bukkit.inventory.ItemStack -import org.bukkit.inventory.meta.Repairable -import xyz.alexcrea.group.ConflictType -import kotlin.math.min - -/** - * Listener for anvil events - */ -class AnvilEventListener : Listener { - - companion object { - // Anvil's output slot - private const val ANVIL_INPUT_LEFT = 0 - private const val ANVIL_INPUT_RIGHT = 1 - private const val ANVIL_OUTPUT_SLOT = 2 - } - - /** - * Event handler logic for when an anvil contains items to be combined - */ - @EventHandler(priority = HIGHEST) - fun anvilCombineCheck(event: PrepareAnvilEvent) { - val inventory = event.inventory - val first = inventory.getItem(ANVIL_INPUT_LEFT) ?: return - val second = inventory.getItem(ANVIL_INPUT_RIGHT) ?: return - if (first.canMergeWith(second)) { - // Should find player - val player = event.view.player - - val newEnchants = first.findEnchantments() - .combineWith(second.findEnchantments(), first.type, player) - val resultItem = first.clone() - resultItem.setEnchantmentsUnsafe(newEnchants) - - var anvilCost = calculateCost(first, second, resultItem) - if (!first.isBook() && !second.isBook()) { - // we only need to be concerned with repair when neither item is a book - val repaired = resultItem.repairFrom(first, second) - anvilCost += if(repaired) ConfigOptions.itemRepairCost else 0 - } - - // Test if nothing change and stop. - if(first == resultItem){ - event.result = null - return - } - - // Rename item and add renaming cost - resultItem.itemMeta?.let { - if(!it.displayName.contentEquals(inventory.renameText)){ - it.setDisplayName(inventory.renameText) - anvilCost += ConfigOptions.itemRenameCost - } - resultItem.itemMeta = it - } - - if (ConfigOptions.limitRepairCost) { - anvilCost = min(anvilCost, ConfigOptions.limitRepairValue) - } - - event.result = resultItem - - /* Because Minecraft likes to have the final say in the repair cost displayed - * we need to wait for the event to end before overriding it, this ensures that - * we have the final say in the process. */ - UnsafeEnchants.instance - .server - .scheduler - .runTask(UnsafeEnchants.instance, Runnable { - if (ConfigOptions.removeRepairLimit) { - inventory.maximumRepairCost = Int.MAX_VALUE - } - inventory.repairCost = anvilCost - event.view.setProperty(REPAIR_COST, anvilCost) - }) - } - } - - /** - * Event handler logic for when a player is trying to pull an item out of the anvil - */ - @EventHandler(ignoreCancelled = true) - fun anvilExtractionCheck(event: InventoryClickEvent) { - //val player = event.whoClicked as? Player ?: return - val inventory = event.inventory as? AnvilInventory ?: return - if (event.rawSlot != ANVIL_OUTPUT_SLOT) { return } - val output = inventory.getItem(ANVIL_OUTPUT_SLOT) ?: return - // Is true if there was no change. probably when there are conflict - if(output == inventory.getItem(ANVIL_INPUT_LEFT)){ - event.result = Event.Result.DENY - return - } - event.result = Event.Result.ALLOW - } - - /** - * Function to calculate most of the xp requirement for the anvil fuse - * Change result work penalty for future use - */ - private fun calculateCost(left: ItemStack, right: ItemStack, result: ItemStack): Int{ - // Extracted From https://minecraft.fandom.com/wiki/Anvil_mechanics#Enchantment_equation - // Calculate work penality - val leftPenality = (left.itemMeta as? Repairable)?.repairCost ?: 0 - val rightPenality = (right.itemMeta as? Repairable)?.repairCost ?: 0 - - // Calculate right value and illegal enchant penalty - var rightValue = 0 - var illegalPenalty = 0 - - val rightIsFormBook = right.isBook() - val resultEnchs = result.findEnchantments() - val resultEnchsKeys = HashSet(resultEnchs.keys) - - for (enchantment in right.findEnchantments()) { - // count enchant as illegal enchant if it conflicts with another enchant or not in result - if((enchantment.key !in resultEnchsKeys)){ - resultEnchsKeys.add(enchantment.key) - val conflictType = UnsafeEnchants.conflictManager.isConflicting(resultEnchsKeys,result.type,enchantment.key) - resultEnchsKeys.remove(enchantment.key) - - if(ConflictType.BIG_CONFLICT == conflictType){ - illegalPenalty += ConfigOptions.sacrificeIllegalCost - } - continue - } - // We know "enchantment.key in resultEnchs" true - val resultLevel = resultEnchs[enchantment.key]!! - - val enchantmentMultiplier = ConfigOptions.enchantmentValue(enchantment.key, rightIsFormBook) - val value = resultLevel * enchantmentMultiplier - UnsafeEnchants.log("Value for ${enchantment.key.enchantmentName} level ${enchantment.value} is $value") - rightValue+=value - - } - - // Try to set work penality for the result item - result.itemMeta?.let { - (it as? Repairable)?.repairCost = leftPenality*2+1 - result.itemMeta = it - } - - UnsafeEnchants.log("Calculated cost: " + - "leftPenality: $leftPenality, " + - "rightPenality: $rightPenality, " + - "rightValue: $rightValue, " + - "illegalPenalty: $illegalPenalty," + - "result penality: ${(result.itemMeta as? Repairable)?.repairCost ?: "none"}") - - // We are missing [Renaming Cost] + [Refilling Durability] but it will be handled later - return rightValue + leftPenality + rightPenality + illegalPenalty - } - -} diff --git a/src/main/kotlin/io/delilaheve/CustomAnvil.kt b/src/main/kotlin/io/delilaheve/CustomAnvil.kt new file mode 100644 index 0000000..4d99faf --- /dev/null +++ b/src/main/kotlin/io/delilaheve/CustomAnvil.kt @@ -0,0 +1,323 @@ +package io.delilaheve + +import io.delilaheve.util.ConfigOptions +import org.bukkit.Bukkit +import org.bukkit.configuration.file.YamlConfiguration +import org.bukkit.plugin.java.JavaPlugin +import xyz.alexcrea.cuanvil.api.event.CAConfigReadyEvent +import xyz.alexcrea.cuanvil.api.event.CAEnchantRegistryReadyEvent +import xyz.alexcrea.cuanvil.command.CustomAnvilCommand +import xyz.alexcrea.cuanvil.command.EditConfigExecutor +import xyz.alexcrea.cuanvil.command.ReloadExecutor +import xyz.alexcrea.cuanvil.config.ConfigHolder +import xyz.alexcrea.cuanvil.dependency.DependencyManager +import xyz.alexcrea.cuanvil.dependency.MinecraftVersionUtil +import xyz.alexcrea.cuanvil.dependency.economy.EconomyManager +import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil +import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry +import xyz.alexcrea.cuanvil.gui.config.MainConfigGui +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant +import xyz.alexcrea.cuanvil.listener.AnvilCloseListener +import xyz.alexcrea.cuanvil.listener.AnvilResultListener +import xyz.alexcrea.cuanvil.listener.ChatEventListener +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener +import xyz.alexcrea.cuanvil.update.ModrinthUpdateChecker +import xyz.alexcrea.cuanvil.update.PluginSetDefault +import xyz.alexcrea.cuanvil.update.UpdateHandler +import xyz.alexcrea.cuanvil.util.MetricsUtil +import java.io.File +import java.io.FileReader +import java.util.logging.Level + +/** + * Bukkit/Spigot/Paper plugin to alter anvil feature + */ +open class CustomAnvil : JavaPlugin() { + + companion object { + // pluginIDS + private const val modrinthPluginID = "S75Ueiq9" + + // Permission string required to use the plugin's features + const val affectedByPluginPermission = "ca.affected" + + // Permission string required to bypass enchantment conflicts test + const val bypassFusePermission = "ca.bypass.fuse" + + // Permission string required to bypass enchantment conflicts test + const val bypassLevelPermission = "ca.bypass.level" + + // Permission string required to reload the config + const val commandReloadPermission = "ca.command.reload" + + // Permission string required to get diagnostic data + const val diagnosticPermission = "ca.command.diagnostic" + + // Permission string required to edit the plugin's config + const val editConfigPermission = "ca.config.edit" + + + // Command Name to reload the config + const val commandReloadName = "anvilconfigreload" + + // Config command name + const val commandConfigName = "customanvilconfig" + + // Current plugin instance + lateinit var instance: CustomAnvil + + // Chat message listener + lateinit var chatListener: ChatEventListener + + var latestVer: String? = null + + /** + * Logging handler + */ + @JvmStatic fun log(message: String) { + if (ConfigOptions.debugLog) { + instance.logger.info(message) + } + } + + /** + * Vebose Logging handler + */ + fun verboseLog(message: String) { + if (ConfigOptions.verboseDebugLog) { + instance.logger.info(message) + } + } + + } + + // stop plugin if we do not force a dirty start (true by default) + // Return true if start was stopped + private fun tryDirtyStart(): Boolean { + if(!ConfigHolder.DEFAULT_CONFIG.config.getBoolean("dirty_start", false)) { + Bukkit.getPluginManager().disablePlugin(this) + return true + } + return false + } + + // stop plugin if we force a safe start (false by default) + // Return true if start was stopped + private fun trySafeStart(): Boolean { + if(ConfigHolder.DEFAULT_CONFIG.config.getBoolean("safe_start", false)) { + Bukkit.getPluginManager().disablePlugin(this) + return true + } + return false + } + + /** + * Setup plugin for use + */ + override fun onEnable() { + instance = this + try { + legacyCheck() + } catch (e: Exception) { + logger.log(Level.SEVERE, "error trying to check for legacy system", e) + MetricsUtil.trackError(e) + if(trySafeStart()) return + } + + // Add commands + try { + prepareCommand() + } catch (e: Exception) { + logger.log(Level.SEVERE, "error trying to register commands", e) + MetricsUtil.trackError(e) + if(trySafeStart()) return + } + + // Load default configuration + try { + if(!ConfigHolder.loadDefaultConfig()) + throw RuntimeException("Error loading configuration file") + } catch (e: Exception) { + logger.log(Level.SEVERE, "error occurred loading default configuration", e) + MetricsUtil.trackError(e) + if(tryDirtyStart()) return + } + + // Load dependency + try { + DependencyManager.loadDependency() + } catch (e: Exception) { + logger.log(Level.SEVERE, "error loading dependency compatibility", e) + MetricsUtil.trackError(e) + if(tryDirtyStart()) return + } + + // Register listeners + try { + registerListeners() + } catch (e: Exception) { + logger.log(Level.SEVERE, "error registering listeners", e) + MetricsUtil.trackError(e) + if(tryDirtyStart()) return + } + + // Load metrics + MetricsUtil.loadMetrics(this) + + // Load other thing later. + // It is so other dependent plugins can implement there event listener before we fire them. + DependencyManager.scheduler.scheduleGlobally(this) { loadEnchantmentSystemDirty() } + } + + override fun onDisable() { + MetricsUtil.shutdownMetrics() + } + + private fun loadEnchantmentSystemDirty() { + try { + loadEnchantmentSystem() + } catch (e: Exception) { + logger.log(Level.SEVERE, "error initializing enchantment system", e) + MetricsUtil.trackError(e) + tryDirtyStart() + } + } + + private fun legacyCheck() { + // Disable old plugin name if exist + val potentialPlugin = Bukkit.getPluginManager().getPlugin("UnsafeEnchantsPlus") + if (potentialPlugin != null) { + Bukkit.getPluginManager().disablePlugin(potentialPlugin) + logger.warning("An old version of this plugin was detected") + logger.warning("Please note CustomAnvil is a more recent version of UnsafeEnchantsPlus") + } + + val isPaper = PlatformUtil.isPaper + if(!isPaper) { + logger.warning("It seems you are using spigot") + logger.warning("Please take notice that spigot is less supported than paper and derivatives") + if(MinecraftVersionUtil.isTooNewForSpigot) { + logger.warning("If replace too expensive is not working this is likely because of spigot") + logger.warning("As native nms is not supported for spigot starting 26.1") + } + } + + val loader = if(isPaper) "paper" else "spigot" + + val version = description.version + val featured = if(version.contains("dev")) null else true + + ModrinthUpdateChecker(modrinthPluginID, loader, null) + .setFeatured(featured) + .setOnError { + logger.log(Level.WARNING, "error trying to fetch latest update", it) + } + .checkVersion { latestVer: String? -> + CustomAnvil.latestVer = latestVer + if(latestVer == null || version.contains(latestVer)) return@checkVersion + + logger.warning("An update may be available: $latestVer") + } + } + + private fun registerListeners() { + // Register chat listener + chatListener = ChatEventListener() + server.pluginManager.registerEvents(chatListener, this) + + // Register anvil events + server.pluginManager.registerEvents(PrepareAnvilListener(), this) + server.pluginManager.registerEvents(AnvilResultListener(), this) + server.pluginManager.registerEvents(AnvilCloseListener(DependencyManager.packetManager), this) + } + + private fun loadEnchantmentSystem(){ + // Register enchantments + CAEnchantmentRegistry.getInstance().registerBukkit() + DependencyManager.registerEnchantments() + + val enchantReadyEvent = CAEnchantRegistryReadyEvent() + server.pluginManager.callEvent(enchantReadyEvent) + + // Load config + if (!ConfigHolder.loadNonDefaultConfig()) { + logger.log(Level.SEVERE,"Plugin has an issue while trying to load non default config... exiting...") + server.pluginManager.disablePlugin(this) + return + } + + // Handle minecraft and plugin updates + UpdateHandler.handleUpdates() + + // Register enchantment of compatible plugin and load configuration change. + DependencyManager.handleCompatibilityConfig() + + // Call config event + val configReadyEvent = CAConfigReadyEvent() + server.pluginManager.callEvent(configReadyEvent) + + // Load gui constants //TODO maybe something better later + MainConfigGui.getInstance().init(DependencyManager.packetManager) + GuiSharedConstant.loadConstants() + + // Prepare economy if possible + EconomyManager.setupEconomy(this) + + // Finally, re add default we may be missing + PluginSetDefault.reAddMissingDefault() + } + + fun reloadResource( + resourceName: String, + hardFailSafe: Boolean = true + ): YamlConfiguration? { + // Save default resource + val file = File(dataFolder, resourceName) + if (!file.exists()) { + saveResource(resourceName, false) + } + + return reloadResource(file, hardFailSafe) + } + + // Unlike above function. this function will not clone default from jar. + fun reloadResource( + resourceFile: File, + hardFailSafe: Boolean = true + ): YamlConfiguration? { + // Test if file exist + if (!resourceFile.exists()) { + return null + } + + // Load resource + val yamlConfig = YamlConfiguration() + try { + val configReader = FileReader(resourceFile) + yamlConfig.load(configReader) + } catch (test: Exception) { + if (hardFailSafe) { + // This is important and may impact gameplay if it does not load. + // Failsafe is to stop the plugin + logger.severe("Resource ${resourceFile.path} Could not be load or reload.") + logger.severe("Disabling plugin.") + Bukkit.getPluginManager().disablePlugin(this) + } else { + logger.warning("Resource ${resourceFile.path} Could not be load or reload.") + } + return null + } + return yamlConfig + } + + fun prepareCommand() { + var command = getCommand(commandReloadName) + command?.setExecutor(ReloadExecutor()) + + command = getCommand(commandConfigName) + command?.setExecutor(EditConfigExecutor()) + + CustomAnvilCommand(this) + } + +} diff --git a/src/main/kotlin/io/delilaheve/UnsafeEnchants.kt b/src/main/kotlin/io/delilaheve/UnsafeEnchants.kt deleted file mode 100644 index b8c925c..0000000 --- a/src/main/kotlin/io/delilaheve/UnsafeEnchants.kt +++ /dev/null @@ -1,124 +0,0 @@ -package io.delilaheve - -import io.delilaheve.util.ConfigOptions -import org.bukkit.Bukkit -import org.bukkit.configuration.file.YamlConfiguration -import org.bukkit.plugin.java.JavaPlugin -import xyz.alexcrea.group.EnchantConflictManager -import xyz.alexcrea.group.ItemGroupManager -import xyz.alexcrea.group.util.Metrics -import xyz.alexcrea.group.util.Metrics.SimplePie -import java.io.File -import java.io.FileReader - -/** - * Bukkit/Spigot/Paper plugin to alter enchantment max - * levels and allow unsafe enchantment combinations - */ -class UnsafeEnchants : JavaPlugin() { - - companion object { - // bstats plugin id - private const val bstatsPluginId = 20923 - - // Permission string required to use the plugin's features - const val unsafePermission = "ue.unsafe" - // Permission string required to bypass enchantment conflicts test - const val bypassFusePermission = "ue.bypass.fuse" - // Permission string required to bypass enchantment conflicts test - const val bypassLevelPermission = "ue.bypass.level" - - // Item Grouping Configuration file name - const val itemGroupingConfigName = "item_groups.yml" - // Conflict Configuration file name - const val enchantConflicConfigName = "enchant_conflict.yml" - - // Current plugin instance - lateinit var instance: UnsafeEnchants - // Current item grouping configuration instance - lateinit var conflictManager: EnchantConflictManager - - /** - * Logging handler - */ - fun log(message: String) { - if (ConfigOptions.debugLog) { - instance.logger.info(message) - } - } - } - - /** - * Setup plugin for use - */ - override fun onEnable() { - instance = this - // Load bstats - val metric = Metrics(this, bstatsPluginId) - saveDefaultConfig() - - addCustomMetric(metric) - - // Load material grouping config - val itemGroupConfig = reloadResource(itemGroupingConfigName) ?: return - // Read material groups from config - val itemGroupsManager = ItemGroupManager() - itemGroupsManager.prepareGroups(itemGroupConfig) - - // Load enchantment conflicts config - val conflictConfig = reloadResource(enchantConflicConfigName) ?: return - // Read conflicts from config and material group manager - conflictManager = EnchantConflictManager() - conflictManager.prepareConflicts(conflictConfig,itemGroupsManager) - - server.pluginManager.registerEvents( - AnvilEventListener(), - this - ) - - } - - private fun addCustomMetric(metric: Metrics) { - metric.addCustomChart(SimplePie("item_rename_cost") { - ConfigOptions.itemRenameCost.toString() - }) - println("item_rename_cost: ${ConfigOptions.itemRenameCost}") - metric.addCustomChart(SimplePie("item_repair_cost") { - ConfigOptions.itemRepairCost.toString() - }) - println("item_repair_cost: ${ConfigOptions.itemRepairCost}") - metric.addCustomChart(SimplePie("sacrifice_illegal_enchant_cost") { - ConfigOptions.sacrificeIllegalCost.toString() - }) - println("sacrifice_illegal_enchant_cost: ${ConfigOptions.sacrificeIllegalCost}") - - } - - private fun reloadResource(resourceName: String, - hardFailSafe:Boolean = true): YamlConfiguration?{ - // Save default resource - val file = File(dataFolder,resourceName) - if(!file.exists()){ - saveResource(resourceName,false) - } - // Load resource - val yamlConfig = YamlConfiguration() - try { - val configReader = FileReader(file) - yamlConfig.load(configReader) - } catch (test: Exception){ - if(hardFailSafe){ - // This is important and may impact gameplay if it does not load. - // Failsafe is to stop the plugin - logger.severe("Resource $resourceName Could not be load or reload.") - logger.severe("Disabling plugin.") - Bukkit.getPluginManager().disablePlugin(this) - }else{ - logger.warning("Resource $resourceName Could not be load or reload.") - } - return null - } - return yamlConfig - } - -} diff --git a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt index c142495..9dc85f9 100644 --- a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt +++ b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt @@ -1,97 +1,232 @@ package io.delilaheve.util -import io.delilaheve.UnsafeEnchants +import io.delilaheve.CustomAnvil import io.delilaheve.util.EnchantmentUtil.enchantmentName -import org.bukkit.enchantments.Enchantment +import org.bukkit.NamespacedKey +import org.bukkit.entity.HumanEntity +import xyz.alexcrea.cuanvil.anvil.AnvilUseType +import xyz.alexcrea.cuanvil.config.ConfigHolder +import xyz.alexcrea.cuanvil.config.WorkPenaltyType +import xyz.alexcrea.cuanvil.config.WorkPenaltyType.WorkPenaltyPart +import xyz.alexcrea.cuanvil.dependency.DependencyManager +import xyz.alexcrea.cuanvil.dependency.economy.EconomyManager +import xyz.alexcrea.cuanvil.enchant.CAEnchantment +import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil +import java.math.BigDecimal +import java.util.* /** * Config option accessors */ object ConfigOptions { - // Path for default enchantment limits - private const val DEFAULT_LIMIT_PATH = "default_limit" - // Path for limiting repair cost - private const val LIMIT_REPAIR_COST = "limit_repair_cost" - // Path for repair value limit - private const val LIMIT_REPAIR_VALUE = "limit_repair_value" - // Path for level cost on item repair - private const val ITEM_REPAIR_COST = "item_repair_cost" - // Path for level cost on item renaming - private const val ITEM_RENAME_COST = "item_rename_cost" - // Path for level cost on illegal enchantment on sacrifice - private const val SACRIFICE_ILLEGAL_COST = "sacrifice_illegal_enchant_cost" - // Path for removing repair cost limits - private const val REMOVE_REPAIR_LIMIT = "remove_repair_limit" - // Root path for enchantment limits - private const val ENCHANT_LIMIT_ROOT = "enchant_limits" - // Root path for enchantment values - private const val ENCHANT_VALUES_ROOT = "enchant_values" + // ---------------------- + // Path for config values + // ---------------------- + + const val METRIC_TYPE = "metric_type" + const val METRIC_COLLECT_ERROR = "metric_collect_errors" + + const val CAP_ANVIL_COST = "limit_repair_cost" + const val MAX_ANVIL_COST = "limit_repair_value" + const val REMOVE_ANVIL_COST_LIMIT = "remove_repair_limit" + + const val REPLACE_TOO_EXPENSIVE = "replace_too_expensive" + + const val ITEM_REPAIR_COST = "item_repair_cost" + const val UNIT_REPAIR_COST = "unit_repair_cost" + + const val ITEM_RENAME_COST = "item_rename_cost" + + const val SACRIFICE_ILLEGAL_COST = "sacrifice_illegal_enchant_cost" + const val ADD_BOOK_ENCHANTMENT_AS_STORED_ENCHANTMENT = "add_book_enchantment_as_stored_enchantment" + + // Color related config + const val ALLOW_COLOR_CODE = "allow_color_code" + const val ALLOW_HEXADECIMAL_COLOR = "allow_hexadecimal_color" + const val ALLOW_MINIMESSAGE = "allow_minimessage" + const val PERMISSION_NEEDED_FOR_COLOR = "permission_needed_for_color" + const val USE_OF_COLOR_COST = "use_of_color_cost" + + const val PER_COLOR_CODE_PERMISSION = "per_color_code_permission" + + // Work penalty config + const val WORK_PENALTY_ROOT = "work_penalty" + const val WORK_PENALTY_INCREASE = "shared_increase" + const val WORK_PENALTY_ADDITIVE = "shared_additive" + const val EXCLUSIVE_WORK_PENALTY_INCREASE = "exclusive_increase" + const val EXCLUSIVE_WORK_PENALTY_ADDITIVE = "exclusive_additive" + + // Enchant limit config + const val ENCHANT_COUNT_LIMIT_ROOT = "enchantment_count_limit" + const val ENCHANT_COUNT_LIMIT_DEFAULT = "$ENCHANT_COUNT_LIMIT_ROOT.default" + const val ENCHANT_COUNT_LIMIT_ITEMS = "$ENCHANT_COUNT_LIMIT_ROOT.items" + + const val ENCHANT_LIMIT_ROOT = "enchant_limits" + const val ENCHANT_VALUES_ROOT = "enchant_values" + + // Dialog menu rename + const val DIALOG_RENAME_ENABLED = "enable_dialog_rename" + const val DIALOG_MAX_SIZE = "dialog_rename_max_size" + const val DIALOG_RENAME_USE_PERMISSION = "permission_needed_for_dialog_rename" + const val DIALOG_KEEP_USER_TEXT = "dialog_rename_keep_user_text" + + // Others + const val DISABLE_MERGE_OVER_ROOT = "disable-merge-over" + + const val IMMUTABLE_ENCHANTMENT_LIST = "immutable_enchantments" + + // Monetary configs + const val MONETARY_USAGE_ROOT = "monetary_cost" + const val SHOULD_USE_MONEY = "$MONETARY_USAGE_ROOT.enabled" + const val MONEY_CURRENCY = "$MONETARY_USAGE_ROOT.currency" + const val MONETARY_MULTIPLIER_ROOT = "$MONETARY_USAGE_ROOT.multipliers" + // Keys for specific enchantment values private const val KEY_BOOK = "book" private const val KEY_ITEM = "item" - // Debug logging toggle path - private const val DEBUG_LOGGING = "debug_log" - // Default value for enchantment limits - private const val DEFAULT_ENCHANT_LIMIT = 5 - // Default value for limiting repair cost - private const val DEFAULT_LIMIT_REPAIR = true - // Default value for repair cost limit - private const val DEFAULT_LIMIT_REPAIR_VALUE = 39 - // Default value for level cost on item repair - private const val DEFAULT_ITEM_REPAIR_COST = 2 - // Default value for level cost on item renaming - private const val DEFAULT_ITEM_RENAME_COST = 1 - // Default value for level cost on illegal enchantment on sacrifice - private const val DEFAULT_SACRIFICE_ILLEGAL_COST = 1 + // Debug flag + const val DEBUG_LOGGING = "debug_log" + const val VERBOSE_DEBUG_LOGGING = "debug_log_verbose" + + // ---------------------- + // Default config values + // ---------------------- + + const val DEFAULT_CAP_ANVIL_COST = false + const val DEFAULT_MAX_ANVIL_COST = 39 + const val DEFAULT_REMOVE_ANVIL_COST_LIMIT = false + + const val DEFAULT_REPLACE_TOO_EXPENSIVE = false + + const val DEFAULT_ITEM_REPAIR_COST = 2 + const val DEFAULT_UNIT_REPAIR_COST = 1 + + const val DEFAULT_ITEM_RENAME_COST = 1 + + const val DEFAULT_SACRIFICE_ILLEGAL_COST = 1 + const val DEFAULT_ADD_BOOK_ENCHANTMENT_AS_STORED_ENCHANTMENT = false + + const val DEFAULT_ENCHANT_COUNT_LIMIT = -1 + + // Color related config + const val DEFAULT_ALLOW_COLOR_CODE = false + const val DEFAULT_ALLOW_HEXADECIMAL_COLOR = false + const val DEFAULT_ALLOW_MINIMESSAGE = false + const val DEFAULT_PERMISSION_NEEDED_FOR_COLOR = true + const val DEFAULT_USE_OF_COLOR_COST = 0 + + const val DEFAULT_PER_COLOR_CODE_PERMISSION = false + + // Monetary configs + const val DEFAULT_SHOULD_USE_MONEY = false + const val DEFAULT_MONEY_CURRENCY = "default" + const val DEFAULT_MONEY_MULTIPLIER = 1.0 + + // Debug flag + private const val DEFAULT_DEBUG_LOG = false + private const val DEFAULT_VERBOSE_DEBUG_LOG = false + + // Dialog menu rename + const val DEFAULT_DIALOG_RENAME_ENABLED = false + const val DEFAULT_DIALOG_MAX_SIZE = 256 + const val DEFAULT_DIALOG_RENAME_USE_PERMISSION = false + const val DEFAULT_DIALOG_KEEP_USER_TEXT = true + + // ------------- + // Config Ranges + // ------------- + // Valid range for repair cost limit - private val REPAIR_LIMIT_RANGE = 1..39 + @JvmField + val MAX_ANVIL_COST_RANGE = 0..1000 + // Valid range for repair cost - private val ITEM_REPAIR_COST_RANGE = 0..255 + @JvmField + val REPAIR_COST_RANGE = 0..1000 + // Valid range for rename cost - private val ITEM_RENAME_COST_RANGE = 0..255 + @JvmField + val ITEM_RENAME_COST_RANGE = 0..1000 + // Valid range for illegal enchantment conflict cost - private val SACRIFICE_ILLEGAL_COST_RANGE = 0..255 - // Default for removing repair cost limits - private const val DEFAULT_REMOVE_LIMIT = false + @JvmField + val SACRIFICE_ILLEGAL_COST_RANGE = 0..1000 + + // Valid range for color use cost + @JvmField + val USE_OF_COLOR_COST_RANGE = 0..1000 + + @JvmField + val DIALOG_MAX_SIZE_RANGE = 0..Int.MAX_VALUE + // Valid range for an enchantment limit - private val ENCHANT_LIMIT_RANGE = 1..255 + const val ENCHANT_LIMIT = 255 + + // Valid range for an enchantment count limit + @JvmField + val ENCHANT_COUNT_LIMIT_RANGE = -1..255 + + // -------------- + // Other defaults + // -------------- + // Default value for an enchantment multiplier private const val DEFAULT_ENCHANT_VALUE = 0 - // Default value for debug logging - private const val DEFAULT_DEBUG_LOG = false + + // Default max before merge disabled (negative mean enabled) + const val DEFAULT_MAX_BEFORE_MERGE_DISABLED = -1 + + // ----------- + // Permissions + // ----------- + private const val RENAME_DIALOG_PERMISSION = "ca.rename.dialog" + + // ------------- + // Get methods + // ------------- /** - * Default enchantment limit + * Whether to cap anvil costs */ - private val defaultEnchantLimit: Int + val doCapCost: Boolean get() { - return UnsafeEnchants.instance + return ConfigHolder.DEFAULT_CONFIG .config - .getInt(DEFAULT_LIMIT_PATH, DEFAULT_ENCHANT_LIMIT) + .getBoolean(CAP_ANVIL_COST, DEFAULT_CAP_ANVIL_COST) } /** - * Whether to limit repair costs to the vanilla limit + * Value to limit anvil costs to */ - val limitRepairCost: Boolean + val maxAnvilCost: Int get() { - return UnsafeEnchants.instance + return ConfigHolder.DEFAULT_CONFIG .config - .getBoolean(LIMIT_REPAIR_COST, DEFAULT_LIMIT_REPAIR) + .getInt(MAX_ANVIL_COST, DEFAULT_MAX_ANVIL_COST) + .takeIf { it in MAX_ANVIL_COST_RANGE } + ?: DEFAULT_MAX_ANVIL_COST } /** - * Value to limit repair costs to + * Whether to remove anvil cost limit */ - val limitRepairValue: Int + val doRemoveCostLimit: Boolean get() { - return UnsafeEnchants.instance + return ConfigHolder.DEFAULT_CONFIG .config - .getInt(LIMIT_REPAIR_VALUE, DEFAULT_LIMIT_REPAIR_VALUE) - .takeIf { it in REPAIR_LIMIT_RANGE } - ?: DEFAULT_LIMIT_REPAIR_VALUE + .getBoolean(REMOVE_ANVIL_COST_LIMIT, DEFAULT_REMOVE_ANVIL_COST_LIMIT) + } + + /** + * Whether to remove repair cost limit + */ + val doReplaceTooExpensive: Boolean + get() { + return ConfigHolder.DEFAULT_CONFIG + .config + .getBoolean(REPLACE_TOO_EXPENSIVE, DEFAULT_REPLACE_TOO_EXPENSIVE) } /** @@ -99,19 +234,31 @@ object ConfigOptions { */ val itemRepairCost: Int get() { - return UnsafeEnchants.instance + return ConfigHolder.DEFAULT_CONFIG .config .getInt(ITEM_REPAIR_COST, DEFAULT_ITEM_REPAIR_COST) - .takeIf { it in ITEM_REPAIR_COST_RANGE } + .takeIf { it in REPAIR_COST_RANGE } ?: DEFAULT_ITEM_REPAIR_COST } + /** + * Value of an item repair + */ + val unitRepairCost: Int + get() { + return ConfigHolder.DEFAULT_CONFIG + .config + .getInt(UNIT_REPAIR_COST, DEFAULT_UNIT_REPAIR_COST) + .takeIf { it in REPAIR_COST_RANGE } + ?: DEFAULT_UNIT_REPAIR_COST + } + /** * Value of an item rename */ val itemRenameCost: Int get() { - return UnsafeEnchants.instance + return ConfigHolder.DEFAULT_CONFIG .config .getInt(ITEM_RENAME_COST, DEFAULT_ITEM_RENAME_COST) .takeIf { it in ITEM_RENAME_COST_RANGE } @@ -123,20 +270,165 @@ object ConfigOptions { */ val sacrificeIllegalCost: Int get() { - return UnsafeEnchants.instance + return ConfigHolder.DEFAULT_CONFIG .config .getInt(SACRIFICE_ILLEGAL_COST, DEFAULT_SACRIFICE_ILLEGAL_COST) .takeIf { it in SACRIFICE_ILLEGAL_COST_RANGE } ?: DEFAULT_SACRIFICE_ILLEGAL_COST } + /** - * Whether to remove repair cost limit + * Consider book enchantment as book stored enchantment */ - val removeRepairLimit: Boolean - get() { - return UnsafeEnchants.instance + val addBookEnchantmentAsStoredEnchantment : Boolean + get(){ + return ConfigHolder.DEFAULT_CONFIG .config - .getBoolean(REMOVE_REPAIR_LIMIT, DEFAULT_REMOVE_LIMIT) + .getBoolean(ADD_BOOK_ENCHANTMENT_AS_STORED_ENCHANTMENT, DEFAULT_ADD_BOOK_ENCHANTMENT_AS_STORED_ENCHANTMENT) + } + + /** + * Allow usage of color code + */ + val allowColorCode: Boolean + get() { + return ConfigHolder.DEFAULT_CONFIG + .config + .getBoolean(ALLOW_COLOR_CODE, DEFAULT_ALLOW_COLOR_CODE) + } + + /** + * Allow usage of hexadecimal color + */ + val allowHexadecimalColor: Boolean + get() { + return ConfigHolder.DEFAULT_CONFIG + .config + .getBoolean(ALLOW_HEXADECIMAL_COLOR, DEFAULT_ALLOW_HEXADECIMAL_COLOR) + } + + /** + * Allow usage of minimessage formating + */ + val allowMinimessage: Boolean + get() { + return ConfigHolder.DEFAULT_CONFIG + .config + .getBoolean(ALLOW_MINIMESSAGE, DEFAULT_ALLOW_MINIMESSAGE) + } + + /** + * If one of the color component is enabled + */ + val renameColorPossible: Boolean + get() { + return allowColorCode || allowHexadecimalColor || allowMinimessage + } + + /** + * If players need a permission to use color + */ + val permissionNeededForColor: Boolean + get() { + return ConfigHolder.DEFAULT_CONFIG + .config + .getBoolean(PERMISSION_NEEDED_FOR_COLOR, DEFAULT_PERMISSION_NEEDED_FOR_COLOR) + } + + /** + * Should each color code require a permission + */ + val usePerColorCodePermission: Boolean + get() { + return ConfigHolder.DEFAULT_CONFIG + .config + .getBoolean(PER_COLOR_CODE_PERMISSION, DEFAULT_PER_COLOR_CODE_PERMISSION) + } + + /** + * How many xp should use of color should cost + */ + val useOfColorCost: Int + get() { + return ConfigHolder.DEFAULT_CONFIG + .config + .getInt(USE_OF_COLOR_COST, DEFAULT_USE_OF_COLOR_COST) + .takeIf { it in USE_OF_COLOR_COST_RANGE } + ?: DEFAULT_USE_OF_COLOR_COST + } + + /** + * How work penalties should work + */ + val workPenaltyType: WorkPenaltyType + get() { + val penaltyMap = EnumMap(AnvilUseType::class.java) + + for (type in AnvilUseType.entries) { + penaltyMap[type] = workPenaltyPart(type) + } + + return WorkPenaltyType(penaltyMap) + } + + /** + * How work penalty should work + */ + fun workPenaltyPart(type: AnvilUseType): WorkPenaltyPart { + val config = ConfigHolder.DEFAULT_CONFIG.config + + // Find values + val defaultPenalty = type.defaultPenalty + val section = config.getConfigurationSection(type.path) ?: return defaultPenalty + + val penaltyIncrease = section.getBoolean(WORK_PENALTY_INCREASE, defaultPenalty.penaltyIncrease) + val penaltyAdditive = section.getBoolean(WORK_PENALTY_ADDITIVE, defaultPenalty.penaltyAdditive) + val exclusivePenaltyIncrease = + section.getBoolean(EXCLUSIVE_WORK_PENALTY_INCREASE, defaultPenalty.exclusivePenaltyIncrease) + val exclusivePenaltyAdditive = + section.getBoolean(EXCLUSIVE_WORK_PENALTY_ADDITIVE, defaultPenalty.exclusivePenaltyAdditive) + + return WorkPenaltyPart(penaltyIncrease, penaltyAdditive, exclusivePenaltyIncrease, exclusivePenaltyAdditive) + } + + /** + * Get material enchantment count limit + * + * @return the current enchantment limit. -1 if none + */ + fun getEnchantCountLimit(type: NamespacedKey): Int? { + val limit = materialEnchantCountLimit(type) + + if(limit != null) return limit + if(defaultEnchantCountLimit >= 0) return defaultEnchantCountLimit + + return DependencyManager.ecoEnchantCompatibility?.getEcoLevelLimit() + } + + /** + * Get the material enchantment count limit. + * + * @return The current enchantment limit. -1 if none + */ + private fun materialEnchantCountLimit(type: NamespacedKey): Int? { + val path = "$ENCHANT_COUNT_LIMIT_ITEMS.${type.key.lowercase()}" + if(!ConfigHolder.DEFAULT_CONFIG.config.isInt(path)) + return null + + return ConfigHolder.DEFAULT_CONFIG.config + .getInt(path) + .takeIf { it in ENCHANT_COUNT_LIMIT_RANGE } + } + /** + * User configured default enchantment count limit + */ + val defaultEnchantCountLimit: Int + get() { + return ConfigHolder.DEFAULT_CONFIG + .config + .getInt(ENCHANT_COUNT_LIMIT_DEFAULT, DEFAULT_ENCHANT_COUNT_LIMIT) + .takeIf { it in ENCHANT_COUNT_LIMIT_RANGE } + ?: DEFAULT_ENCHANT_COUNT_LIMIT } /** @@ -144,21 +436,105 @@ object ConfigOptions { */ val debugLog: Boolean get() { - return UnsafeEnchants.instance + return ConfigHolder.DEFAULT_CONFIG .config .getBoolean(DEBUG_LOGGING, DEFAULT_DEBUG_LOG) } + /** + * Whether to show verbose debug logging + */ + val verboseDebugLog: Boolean + get() { + return ConfigHolder.DEFAULT_CONFIG + .config + .getBoolean(VERBOSE_DEBUG_LOGGING, DEFAULT_VERBOSE_DEBUG_LOG) + } + + /** + * Is the dialog menu for rename enabled + */ + val doRenameDialog: Boolean + get() { + return ConfigHolder.DEFAULT_CONFIG + .config + .getBoolean(DIALOG_RENAME_ENABLED, DEFAULT_DIALOG_RENAME_ENABLED) + } + + /** + * Do the dialog menu require permission + */ + val doRenameDialogUsePermission: Boolean + get() { + return ConfigHolder.DEFAULT_CONFIG + .config + .getBoolean(DIALOG_RENAME_USE_PERMISSION, DEFAULT_DIALOG_RENAME_USE_PERMISSION) + } + + fun canUseDialogRename(player: HumanEntity): Boolean { + if(!doRenameDialog || !AnvilRenameDialogUtil.anvilRenameDialog.canSendDialog()) return false + if(doRenameDialogUsePermission && !player.hasPermission(RENAME_DIALOG_PERMISSION)) return false + + return true + } + + /** + * Do the dialog menu require permission + */ + val renameDialogMaxSize: Int + get() { + return ConfigHolder.DEFAULT_CONFIG + .config + .getInt(DIALOG_MAX_SIZE, DEFAULT_DIALOG_MAX_SIZE) + .takeIf { it in DIALOG_MAX_SIZE_RANGE } + ?: Int.MAX_VALUE + } + + /** + * Should the text used for rename should be kept in the item's pdc + */ + val shouldKeepRenameText: Boolean + get() { + return ConfigHolder.DEFAULT_CONFIG + .config + .getBoolean(DIALOG_KEEP_USER_TEXT, DEFAULT_DIALOG_KEEP_USER_TEXT) + } + /** * Get the given [enchantment]'s limit */ - fun enchantLimit(enchantment: Enchantment): Int { - val path = "${ENCHANT_LIMIT_ROOT}.${enchantment.enchantmentName}" - return UnsafeEnchants.instance - .config - .getInt(path, defaultEnchantLimit) - .takeIf { it in ENCHANT_LIMIT_RANGE } - ?: defaultEnchantLimit + fun enchantLimit(enchantment: CAEnchantment): Int { + val limit = rawEnchantLimit(enchantment) + if(limit >= 0) return limit.coerceAtMost(ENCHANT_LIMIT) + + // get default + return enchantment.defaultMaxLevel() + } + + /** + * Get the given [enchantment]'s limit + */ + fun rawEnchantLimit(enchantment: CAEnchantment): Int { + // Test namespace + var limit = enchantLimit(enchantment.key.toString()) + if (limit >= 0) return limit + + // Test legacy (name only) + limit = enchantLimit(enchantment.enchantmentName) + if (limit >= 0) return limit + + // Default to negative + return -1 + } + + /** + * Get the given [enchantmentName]'s limit + */ + private fun enchantLimit(enchantmentName: String): Int { + + val path = "${ENCHANT_LIMIT_ROOT}.$enchantmentName" + return CustomAnvil.instance.config + .getInt(path, -1) } /** @@ -166,16 +542,138 @@ object ConfigOptions { * it's source [isFromBook] */ fun enchantmentValue( - enchantment: Enchantment, + enchantment: CAEnchantment, isFromBook: Boolean ): Int { + // Test namespace + var limit = enchantmentValue(enchantment.key.toString(), isFromBook) + if (limit != null) return limit + + // Test legacy (name only) + limit = enchantmentValue(enchantment.enchantmentName, isFromBook) + if (limit != null) return limit + + // get default (and test old legacy if present) + return getDefaultValue(enchantment, isFromBook) + + } + + /** + * Get the appropriate [enchantmentName]'s value dependent on whether + * it's source [isFromBook] + */ + private fun enchantmentValue( + enchantmentName: String, + isFromBook: Boolean + ): Int? { val typeKey = if (isFromBook) KEY_BOOK else KEY_ITEM - val path = "${ENCHANT_VALUES_ROOT}.${enchantment.enchantmentName}.$typeKey" - return UnsafeEnchants.instance + val path = "${ENCHANT_VALUES_ROOT}.${enchantmentName}.$typeKey" + return CustomAnvil.instance .config - .getInt(path, DEFAULT_ENCHANT_VALUE) + .getInt(path, DEFAULT_ENCHANT_VALUE - 1) .takeIf { it >= DEFAULT_ENCHANT_VALUE } - ?: DEFAULT_ENCHANT_VALUE + } + + /** + * Get default value if enchantment do not exist on config + */ + private fun getDefaultValue( + enchantment: CAEnchantment, // compatibility with 1.20.5. TODO better update system + isFromBook: Boolean + ): Int { + + val enchantmentName = enchantment.key.toString() + if (enchantmentName == "minecraft:sweeping_edge") { + var limit = enchantmentValue("minecraft:sweeping", isFromBook) + if (limit != null) return limit + + // legacy name + limit = enchantmentValue("sweeping", isFromBook) + if (limit != null) return limit + } + + val rarity = enchantment.defaultRarity() + return if (isFromBook) + rarity.bookValue + else + rarity.itemValue + } + + /** + * Get the given [enchantmentName]'s level before merge is disabled + * a negative value would mean never disabled + */ + fun maxBeforeMergeDisabled(enchantment: CAEnchantment): Int { + val key = enchantment.key.toString() + var value = maxBeforeMergeDisabled(key) + if (value >= 0) return value + + // Legacy name + val legacy = enchantment.enchantmentName + value = maxBeforeMergeDisabled(legacy) + if (value >= 0) return value + + if (key == "minecraft:sweeping_edge") { + value = maxBeforeMergeDisabled("minecraft:sweeping") + if (value >= 0) return value + + // legacy name of legacy enchantment name + value = maxBeforeMergeDisabled("sweeping") + if (value >= 0) return value + } + + return DEFAULT_MAX_BEFORE_MERGE_DISABLED + } + + /** + * Get the given [enchantmentName]'s level before merge is disabled + * a negative value would mean never disabled + */ + private fun maxBeforeMergeDisabled(enchantmentName: String): Int { + // find if set + val path = "${DISABLE_MERGE_OVER_ROOT}.$enchantmentName" + + return CustomAnvil.instance + .config + .getInt(path, -1) + } + + fun isImmutable(key: NamespacedKey): Boolean { + val immutables = ConfigHolder.DEFAULT_CONFIG.config.getStringList(IMMUTABLE_ENCHANTMENT_LIST) + + // We need to ignore case so can't just check "contain" + for (ench in immutables) { + if (ench.equals(key.toString(), ignoreCase = true) || + ench.equals(key.key, ignoreCase = true) + ) + return true + } + return false + } + + /* + * Monetary configs (only for 1.21.6+) + * Also require dialog rename + */ + fun shouldUseMoney(player: HumanEntity): Boolean { + return EconomyManager.economy?.initialized() == true && + canUseDialogRename(player) && + ConfigHolder.DEFAULT_CONFIG + .config + .getBoolean(SHOULD_USE_MONEY, DEFAULT_SHOULD_USE_MONEY) + } + + val usedCurrency: String + get() { + return ConfigHolder.DEFAULT_CONFIG + .config + .getString(MONEY_CURRENCY, DEFAULT_MONEY_CURRENCY)!! + } + + fun getMonetaryMultiplier(type: String): BigDecimal { + return BigDecimal(ConfigHolder.DEFAULT_CONFIG + .config + .getDouble("$MONETARY_MULTIPLIER_ROOT.$type", DEFAULT_MONEY_MULTIPLIER)) } } diff --git a/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt b/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt index b60adf2..af959f2 100644 --- a/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt +++ b/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt @@ -1,10 +1,12 @@ package io.delilaheve.util -import io.delilaheve.UnsafeEnchants -import org.bukkit.Material -import org.bukkit.enchantments.Enchantment +import io.delilaheve.CustomAnvil import org.bukkit.entity.HumanEntity -import xyz.alexcrea.group.ConflictType +import org.bukkit.inventory.ItemStack +import xyz.alexcrea.cuanvil.config.ConfigHolder +import xyz.alexcrea.cuanvil.enchant.CAEnchantment +import xyz.alexcrea.cuanvil.group.ConflictType +import xyz.alexcrea.cuanvil.util.MaterialUtil.customType import kotlin.math.max import kotlin.math.min @@ -16,62 +18,110 @@ object EnchantmentUtil { /** * Enchantment name without namespace */ - val Enchantment.enchantmentName: String + val CAEnchantment.enchantmentName: String get() = key.key /** * Combine 2 sets of enchantments according to our configuration */ - fun Map.combineWith( - other: Map, - mat: Material, + fun Map.combineWith( + other: Map, + item: ItemStack, player: HumanEntity - ) = mutableMapOf().apply { + ) = mutableMapOf().apply { putAll(this@combineWith) - other.forEach { (enchantment, level) -> - // Enchantment not yet in result list - if (!containsKey(enchantment)) { - if(player.hasPermission(UnsafeEnchants.unsafePermission)){ - // Add the enchantment if it doesn't have conflicts, or, if player is allowed to bypass enchantment restrictions - this[enchantment] = level - if(!player.hasPermission(UnsafeEnchants.bypassFusePermission) && - (UnsafeEnchants.conflictManager.isConflicting(this.keys,mat,enchantment) != ConflictType.NO_CONFLICT)){ - this.remove(enchantment) - } - }else if(!keys.any { enchantment.conflictsWith(it) }){ - this[enchantment] = level - } + CustomAnvil.verboseLog("Testing merge") + val bypassFuse = player.hasPermission(CustomAnvil.bypassFusePermission) + val bypassLevel = player.hasPermission(CustomAnvil.bypassLevelPermission) + + var maxEnchantCount = ConfigOptions.getEnchantCountLimit(item.customType) + if(maxEnchantCount == null || maxEnchantCount < 0) maxEnchantCount = Int.MAX_VALUE + + val allowed = other.filter { (enchantment, _) -> enchantment.isAllowed(player) } + val new = allowed.filter{ (enchantment, _) -> !containsKey(enchantment)} + val old = allowed.filter{ (enchantment, _) -> containsKey(enchantment)} + + fun maxLevel(enchantment: CAEnchantment): Int { + val max = if (bypassLevel) { 255 } + else { ConfigOptions.enchantLimit(enchantment) } + + CustomAnvil.verboseLog("Max level of ${enchantment.key} is $max (bypassLevel is $bypassLevel)") + return max + } + + old.forEach { (enchantment, level) -> + // Get max level or 255 if player can bypass + val maxLevel = maxLevel(enchantment) + val cappedLevel = min(level, maxLevel) + + val oldLevel = this[enchantment]!! // <- should not be null. (enchantment already in result list) + + // ... and they're not the same level + if (oldLevel != cappedLevel) { + // apply the greater of the two or left one if right is above max + this[enchantment] = max(oldLevel, cappedLevel) } - // Enchantment already in result list - else{ - // ... and they are conflicting - if((UnsafeEnchants.conflictManager.isConflicting(this.keys,mat,enchantment) != ConflictType.NO_CONFLICT) - && !player.hasPermission(UnsafeEnchants.bypassFusePermission)){ - return@forEach + // ... and they're the same level + else { + // We test if it is allowed to merge at this level + if(!bypassLevel){ + val maxBeforeDisabled = ConfigOptions.maxBeforeMergeDisabled(enchantment) + if((maxBeforeDisabled > 0) && (oldLevel >= maxBeforeDisabled)) { + CustomAnvil.verboseLog( + "Reached max merge before disable for ${enchantment.key}: $oldLevel/$maxBeforeDisabled)") + return@forEach + } } - // ... and they're not the same level - if(this[enchantment] != other[enchantment]){ - val newLevel = max(this[enchantment] ?: 0, other[enchantment] ?: 0) - // apply the greater of the two if non-zero - if (newLevel > 0) { this[enchantment] = newLevel } - } - // ... and they're the same level - else { - // try to increase the enchantment level by 1 - var newLevel = this[enchantment]!! +1 - // Get max level or 255 if player can bypass - val maxLevel = if(player.hasPermission(UnsafeEnchants.bypassLevelPermission)){ - 255 - }else{ - ConfigOptions.enchantLimit(enchantment) - } - newLevel = min(newLevel, maxLevel) - if (newLevel > 0) { this[enchantment] = newLevel } + // Now we increase the enchantment level by 1 + var newLevel = oldLevel + 1 + newLevel = max(min(newLevel, maxLevel), oldLevel) + this[enchantment] = newLevel + } + + if(bypassFuse){ + CustomAnvil.verboseLog("Bypassed conflict check for ${enchantment.key}") + } else { + val conflictType = ConfigHolder.CONFLICT_HOLDER.conflictManager + .isConflicting(this, item, enchantment) + + // ... and they are conflicting + if(conflictType != ConflictType.NO_CONFLICT){ + CustomAnvil.verboseLog( + "Enchantment already in result list, and they are conflicting (${enchantment.key}, conflict: $conflictType)") + this[enchantment] = oldLevel + return@forEach } } } + + // Try to add new now + new.forEach { (enchantment, level) -> + // Get max level or 255 if player can bypass + val maxLevel = maxLevel(enchantment) + val cappedLevel = min(level, maxLevel) + + // Do not allow new enchantment if above maximum + if(this.size >= maxEnchantCount) return@forEach + + // Add the enchantment if it doesn't have conflicts, or if player is allowed to bypass enchantment restrictions + this[enchantment] = cappedLevel + if(bypassFuse){ + CustomAnvil.verboseLog("Bypassed conflict check for ${enchantment.key}") + return@forEach + } + + val conflictType = ConfigHolder.CONFLICT_HOLDER.conflictManager + .isConflicting(this, item, enchantment) + + if (conflictType != ConflictType.NO_CONFLICT) { + CustomAnvil.verboseLog("Enchantment not yet in result list, but there is conflict (${enchantment.key}, conflict: $conflictType)") + this.remove(enchantment) + } + + } + } } diff --git a/src/main/kotlin/io/delilaheve/util/ItemUtil.kt b/src/main/kotlin/io/delilaheve/util/ItemUtil.kt index 1f8f9c8..25698ad 100644 --- a/src/main/kotlin/io/delilaheve/util/ItemUtil.kt +++ b/src/main/kotlin/io/delilaheve/util/ItemUtil.kt @@ -1,13 +1,14 @@ package io.delilaheve.util -import io.delilaheve.UnsafeEnchants -import org.bukkit.Material.BOOK import org.bukkit.Material.ENCHANTED_BOOK -import org.bukkit.enchantments.Enchantment import org.bukkit.inventory.ItemStack import org.bukkit.inventory.meta.Damageable -import org.bukkit.inventory.meta.EnchantmentStorageMeta -import org.bukkit.inventory.meta.ItemMeta +import xyz.alexcrea.cuanvil.enchant.CAEnchantment +import xyz.alexcrea.cuanvil.update.UpdateUtils +import xyz.alexcrea.cuanvil.util.MaterialUtil.customType +import xyz.alexcrea.cuanvil.util.MaxDamageCheckerUtil +import kotlin.math.ceil +import kotlin.math.max import kotlin.math.min /** @@ -15,64 +16,33 @@ import kotlin.math.min */ object ItemUtil { - /** - * Check if this [ItemStack] is a [BOOK] or [ENCHANTED_BOOK] - */ - fun ItemStack.isBook() = type in listOf(BOOK, ENCHANTED_BOOK) - /** * Check if this [ItemStack] is an [ENCHANTED_BOOK] */ - private fun ItemStack.isEnchantedBook() = type == ENCHANTED_BOOK - - /** - * Determine if this [ItemStack] can hold enchants, this should be sufficient for - * detecting if an item is a tool/armour/etc... and not a carrot/potato/etc... - */ - private fun ItemStack.canHoldEnchants() = Enchantment.values() - .any { it.canEnchantItem(this) } + fun ItemStack.isEnchantedBook() = type == ENCHANTED_BOOK /** * Find the enchantment map for this [ItemStack] and return it as a [MutableMap] */ - fun ItemStack.findEnchantments() = if (isBook()) { - (itemMeta as? EnchantmentStorageMeta)?.storedEnchants ?: emptyMap() - } else { - itemMeta?.enchants ?: emptyMap() - } + fun ItemStack.findEnchantments(): MutableMap = CAEnchantment.getEnchants(this) /** * Apply an [enchantments] map to this [ItemStack] */ - fun ItemStack.setEnchantmentsUnsafe(enchantments: Map) { - if (isBook()) { - /* For some god-forsaken reason, item meta is not mutable - * so, we have to get the instance, modify it, then set it - * back to the item... #BecauseMinecraft */ - val bookMeta = (itemMeta as? EnchantmentStorageMeta) - bookMeta?.replaceEnchants(enchantments) - itemMeta = bookMeta - } else { - itemMeta?.enchants?.forEach { (enchant, _) -> - removeEnchantment(enchant) - } - addUnsafeEnchantments(enchantments) + fun ItemStack.setEnchantmentsUnsafe(enchantments: Map) { + CAEnchantment.clearEnchants(this) + + enchantments.forEach { (enchantment, level) -> + enchantment.addEnchantmentUnsafe(this, level) } + } - /** - * Apply an [enchantments] map to this book - */ - private fun EnchantmentStorageMeta.replaceEnchants( - enchantments: Map - ) { - storedEnchants.forEach { (enchant, _) -> - removeStoredEnchant(enchant) - } - enchantments.forEach { (enchant, level) -> - val added = addStoredEnchant(enchant, level, true) - UnsafeEnchants.log("${enchant.key} added to item? $added") - } + private fun maxDamage(damageable: Damageable): Int { + val ver = UpdateUtils.currentMinecraftVersion() + if(ver.major <= 1 && ver.minor <= 20 && ver.patch < 5) return Integer.MAX_VALUE + + return MaxDamageCheckerUtil.getMaxDamage(damageable) } /** @@ -87,26 +57,50 @@ object ItemUtil { (itemMeta as? Damageable)?.let { val durability = type.maxDurability.toInt() val firstDamage = (first.itemMeta as? Damageable)?.damage ?: 0 - if( firstDamage == 0) return false + if (firstDamage == 0) return false val firstDurability = durability - firstDamage val secondDamage = (second.itemMeta as? Damageable)?.damage ?: 0 val secondDurability = durability - secondDamage val combinedDurability = firstDurability + secondDurability val newDurability = min(combinedDurability, durability) - it.damage = durability - newDurability - itemMeta = it as ItemMeta + + val maxDamage = maxDamage(it) + it.damage = min(durability - newDurability, maxDamage) + itemMeta = it return true } return false } + fun ItemStack.unitRepair( + unitAmount: Int, + percentPerUnit: Double + ): Int { + (itemMeta as? Damageable)?.let { + val durability = type.maxDurability.toInt() + val firstDamage = it.damage + if (firstDamage == 0) return 0 + var unitCount = 0 + var damage = firstDamage + while ((unitCount < unitAmount) && (damage > 0)) { + unitCount++ + damage = ceil(firstDamage - durability * percentPerUnit * unitCount).toInt() + } + + it.damage = max(damage, 0) + itemMeta = it + return unitCount + } + return 0 + } + /** * Check that this [ItemStack] can merge with the [other] * * The two items should either be the same type, or, the [other] is a book */ fun ItemStack.canMergeWith( - other: ItemStack - ) = type == other.type || (canHoldEnchants() && other.isEnchantedBook()) + other: ItemStack? + ) = (other != null) && (customType == other.customType || (other.isEnchantedBook())) } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilCost.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilCost.kt new file mode 100644 index 0000000..710687c --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilCost.kt @@ -0,0 +1,72 @@ +package xyz.alexcrea.cuanvil.anvil + +import io.delilaheve.util.ConfigOptions +import java.math.BigDecimal +import kotlin.math.min +import io.delilaheve.util.ConfigOptions.getMonetaryMultiplier as moneyMultiplier + +open class AnvilCost { + private val isAlone: Boolean + var valid = true // Get set as invalid if cost can be satisfied + var isMonetary = false + + var generic = 0 + var enchantment = 0 + var repair = 0 + var rename = 0 + var lore = 0 + var illegalPenalty = 0 + var workPenalty = 0 + var recipe = 0 + + constructor(generic: Int) { + this.generic = generic + isAlone = true + } + + constructor() { + isAlone = false + } + + fun asXpCost(): Int { + return generic + enchantment + repair + rename + lore + illegalPenalty + workPenalty + recipe + } + + fun filteredXpCost(ignoreRules: Boolean = false): Int { + val original = asXpCost() + + // Test repair cost limit + return if ( + !ignoreRules && + !ConfigOptions.doRemoveCostLimit && + ConfigOptions.doCapCost + ) { + min(original, ConfigOptions.maxAnvilCost) + } else { + original + } + } + + open fun asMonetaryCost(): BigDecimal { + // multiply by per use type multipliers + return BigDecimal(generic) + .add(BigDecimal(enchantment).multiply(moneyMultiplier("enchantment"))) + .add(BigDecimal(repair).multiply(moneyMultiplier("repair"))) + .add(BigDecimal(rename).multiply(moneyMultiplier("rename"))) + .add(BigDecimal(lore).multiply(moneyMultiplier("lore_edit"))) + .add(BigDecimal(enchantment).multiply(moneyMultiplier("enchantment"))) + .add(BigDecimal(illegalPenalty).multiply(moneyMultiplier("work_penalty"))) + .add(BigDecimal(workPenalty).multiply(moneyMultiplier("work_penalty"))) + .add(BigDecimal(recipe).multiply(moneyMultiplier("recipe"))) + .multiply(moneyMultiplier("global")) + } +} + +class CustomCraftCost(val rawCost: Int): AnvilCost() { + + override fun asMonetaryCost(): BigDecimal { + return BigDecimal(rawCost) + .multiply(moneyMultiplier("global")) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt new file mode 100644 index 0000000..6b106fc --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt @@ -0,0 +1,331 @@ +package xyz.alexcrea.cuanvil.anvil + +import io.delilaheve.CustomAnvil +import io.delilaheve.util.ConfigOptions +import io.delilaheve.util.EnchantmentUtil.combineWith +import io.delilaheve.util.ItemUtil.findEnchantments +import io.delilaheve.util.ItemUtil.isEnchantedBook +import io.delilaheve.util.ItemUtil.repairFrom +import io.delilaheve.util.ItemUtil.setEnchantmentsUnsafe +import io.delilaheve.util.ItemUtil.unitRepair +import org.bukkit.ChatColor +import org.bukkit.Material +import org.bukkit.entity.HumanEntity +import org.bukkit.entity.Player +import org.bukkit.inventory.AnvilInventory +import org.bukkit.inventory.InventoryView +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.ItemMeta +import org.bukkit.persistence.PersistentDataType +import xyz.alexcrea.cuanvil.dependency.DependencyManager +import xyz.alexcrea.cuanvil.dialog.AnvilRenameDialog +import xyz.alexcrea.cuanvil.enchant.CAEnchantment +import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe +import xyz.alexcrea.cuanvil.util.CasedStringUtil +import xyz.alexcrea.cuanvil.util.CustomRecipeUtil +import xyz.alexcrea.cuanvil.util.MaterialUtil.isAir +import xyz.alexcrea.cuanvil.util.MiniMessageUtil +import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair +import xyz.alexcrea.cuanvil.util.anvil.AnvilColorUtil +import xyz.alexcrea.cuanvil.util.anvil.AnvilLoreEditUtil +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.config.LoreEditType +import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil + +object AnvilMergeLogic { + + open class AnvilResult { + companion object { + val EMPTY = AnvilResult(null, AnvilCost()) + } + + val item: ItemStack? + val cost: AnvilCost + val ignoreXpRules: Boolean + + constructor(item: ItemStack?, cost: AnvilCost, ignoreXpRules: Boolean = false) { + this.item = item + this.cost = cost + this.ignoreXpRules = ignoreXpRules + } + + fun isEmpty(): Boolean { + return item == null + } + } + + class UnitRepairResult : AnvilResult { + companion object { + val EMPTY = UnitRepairResult(null, AnvilCost(), 0) + } + + val repairAmount: Int + + constructor(item: ItemStack?, cost: AnvilCost, repairAmount: Int) : super(item, cost) { + this.repairAmount = repairAmount + } + } + + class CustomCraftResult : AnvilResult { + companion object { + val EMPTY = CustomCraftResult(null, CustomCraftCost(0), 0, null) + } + + val customCraftCost: CustomCraftCost + val amount: Int + val recipe: AnvilCustomRecipe? + + constructor( + item: ItemStack?, cost: CustomCraftCost, + amount: Int, recipe: AnvilCustomRecipe? + ) : super(item, cost, true) { + this.customCraftCost = cost + this.amount = amount + this.recipe = recipe + } + } + + class LoreEditResult : AnvilResult { + companion object { + val EMPTY = LoreEditResult(null, AnvilCost(), LoreEditType.APPEND_PAPER) + } + + val type: LoreEditType + + constructor(item: ItemStack?, cost: AnvilCost, type: LoreEditType) : super(item, cost) { + this.type = type + } + } + + fun doRenaming( + view: InventoryView, //TODO use anvil view + inventory: AnvilInventory, + player: Player, first: ItemStack + ): AnvilResult { + val resultItem = DependencyManager.cloneItem(player, first) + val cost = AnvilCost() + cost.rename = handleRename(resultItem, inventory, player) + + // Test/stop if nothing changed. + if (first == resultItem) { + CustomAnvil.log("no right item, But input is same as output") + return AnvilResult.EMPTY + } + + cost.workPenalty = AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.RENAME_ONLY) + val result = + DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.RENAME_ONLY, cost) + + return AnvilResult(result, cost) + } + + private fun processDialogPCD(meta: ItemMeta, player: HumanEntity) { + val text = AnvilRenameDialogUtil.anvilRenameDialog.currentText(player) + return processPCD(meta, player, text) + } + + fun processPCD(meta: ItemMeta, player: HumanEntity, text: String?) { + val keepDialog = ConfigOptions.canUseDialogRename(player) && ConfigOptions.shouldKeepRenameText + + val pdc = meta.persistentDataContainer + if (!keepDialog) + pdc.remove(AnvilRenameDialog.PCD_KEEP_RENAME_TEXT_KEY) + else { + if (text == null || text.isBlank()) + pdc.remove(AnvilRenameDialog.PCD_KEEP_RENAME_TEXT_KEY) + else pdc.set(AnvilRenameDialog.PCD_KEEP_RENAME_TEXT_KEY, PersistentDataType.STRING, text) + } + } + + private fun handleRename(resultItem: ItemStack, inventory: AnvilInventory, player: HumanEntity): Int { + // Can be null + var renameText = ChatColor.stripColor(inventory.renameText) + + var sumCost = 0 + var useColor = false + if (ConfigOptions.renameColorPossible && renameText != null) { + val component = AnvilColorUtil.handleColor( + renameText, + AnvilColorUtil.renamePermission(player) + ) + + if (component != null) { + renameText = MiniMessageUtil.legacy_mm.serialize(component) + + sumCost += ConfigOptions.useOfColorCost + useColor = true + } + } + + // Rename item and add renaming cost + resultItem.itemMeta?.let { + val hasDisplayName = it.hasDisplayName() + val displayName = if (!hasDisplayName) null + else if (useColor) it.displayName + else ChatColor.stripColor(it.displayName) + + + if (!displayName.contentEquals(renameText) && !(displayName == null && + renameText == "" || + //TODO on recent paper check effective name instead + renameText == CasedStringUtil.snakeToUpperSpacedCase(resultItem.type.name.lowercase()) + ) + ) { + it.setDisplayName(renameText) + processDialogPCD(it, player) + resultItem.itemMeta = it + + sumCost += ConfigOptions.itemRenameCost + } + + return sumCost + } + return 0 + } + + fun doMerge( + view: InventoryView, //TODO use anvil view instead + inventory: AnvilInventory, + player: Player, + first: ItemStack, second: ItemStack + ): AnvilResult { + val newEnchants = first.findEnchantments() + .combineWith(second.findEnchantments(), first, player) + var hasChanged = !isIdentical(first.findEnchantments(), newEnchants) + + val resultItem = DependencyManager.cloneItem(player, first) + val cost = AnvilCost() + if (hasChanged) { + resultItem.setEnchantmentsUnsafe(newEnchants) + // Calculate enchantment cost + AnvilXpUtil.getRightValues(second, resultItem, cost) + } + + // Calculate repair cost + if (!first.isEnchantedBook() && !second.isEnchantedBook()) { + // we only need to be concerned with repair when neither item is a book + val repaired = resultItem.repairFrom(first, second) + cost.repair = if (repaired) ConfigOptions.itemRepairCost else 0 + hasChanged = hasChanged || repaired + } + + // Test/stop if nothing changed. + if (!hasChanged) { + CustomAnvil.log("Mergeable with second, But input is same as output") + return AnvilResult.EMPTY + } + // As calculatePenalty edit result, we need to calculate penalty after checking equality + cost.workPenalty = AnvilXpUtil.calculatePenalty(first, second, resultItem, AnvilUseType.MERGE) + // Calculate rename cost + cost.rename = handleRename(resultItem, inventory, player) + + val result = + DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.MERGE, cost) + + return AnvilResult(result, cost) + } + + private fun isIdentical( + firstEnchants: MutableMap, + resultEnchants: MutableMap + ): Boolean { + if (firstEnchants.size != resultEnchants.size) return false + for (entry in resultEnchants) { + if (firstEnchants.getOrDefault(entry.key, entry.value - 1) != entry.value) return false + } + + return true + } + + // return true if a custom recipe exist with these ingredients + fun testCustomRecipe( + view: InventoryView, //TODO use anvil view instead + inventory: AnvilInventory, + player: Player, + first: ItemStack, second: ItemStack? + ): CustomCraftResult { + val recipe = CustomRecipeUtil.getCustomRecipe(first, second) + CustomAnvil.verboseLog("custom recipe not null? ${recipe != null}") + if (recipe == null) return CustomCraftResult.EMPTY + + val amount = CustomRecipeUtil.getCustomRecipeAmount(recipe, first, second) + + val resultItem: ItemStack = DependencyManager.cloneItem(player, recipe.resultItem!!) + resultItem.amount *= amount + + // Maybe add an option on custom craft to ignore/not ignore penalty ?? + val xpCost = recipe.determineCost(amount, first, resultItem) + + val cost = CustomCraftCost(xpCost) + // This is for displayed cost + cost.recipe = if (recipe.removeExactLinearXp) AnvilXpUtil.calculateMinimumLevelForXp(xpCost) + else AnvilXpUtil.calculateLevelForXp(xpCost) + + val result = + DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.CUSTOM_CRAFT, cost) + return CustomCraftResult(result, cost, amount, recipe) + } + + fun testUnitRepair( + view: InventoryView, //TODO use anvil view + inventory: AnvilInventory, + player: Player, + first: ItemStack, second: ItemStack + ): UnitRepairResult { + val unitRepairAmount = first.getRepair(second) ?: return UnitRepairResult.EMPTY + + return testUnitRepair(view, inventory, player, first, second, unitRepairAmount) + } + + fun testUnitRepair( + view: InventoryView, //TODO use anvil view instead + inventory: AnvilInventory, + player: Player, + first: ItemStack, second: ItemStack, + unitRepairAmount: Double + ): UnitRepairResult { + val resultItem = DependencyManager.cloneItem(player, first) + val cost = AnvilCost() + cost.rename = handleRename(resultItem, inventory, player) + + val repairAmount = resultItem.unitRepair(second.amount, unitRepairAmount) + if (repairAmount > 0) + cost.repair = repairAmount * ConfigOptions.unitRepairCost + + // We do not care about right item penalty for unit repair + cost.workPenalty = AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.UNIT_REPAIR) + + // Test/stop if nothing changed. + if (first == resultItem) { + CustomAnvil.log("unit repair, But input is same as output") + return UnitRepairResult.EMPTY + } + + val result = + DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.UNIT_REPAIR, cost) + return UnitRepairResult(result, cost, repairAmount) + } + + fun testLoreEdit( + player: Player, + first: ItemStack, second: ItemStack + ): LoreEditResult { + val type = second.type + + val result = if (Material.WRITABLE_BOOK == type) + AnvilLoreEditUtil.tryLoreEditByBook(player, first, second) + else if (Material.PAPER == type) + AnvilLoreEditUtil.tryLoreEditByPaper(player, first, second) + else LoreEditResult.EMPTY + + if (result.isEmpty()) return result + + if (result.item!!.isAir || first == result.item) { + CustomAnvil.log("lore edit, But input is same as output") + return LoreEditResult.EMPTY + } + + return result + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilUseType.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilUseType.kt new file mode 100644 index 0000000..67782f3 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilUseType.kt @@ -0,0 +1,67 @@ +package xyz.alexcrea.cuanvil.anvil + +import org.bukkit.Material +import xyz.alexcrea.cuanvil.config.WorkPenaltyType +import xyz.alexcrea.cuanvil.util.anvil.AnvilUseTypeUtil + +enum class AnvilUseType( + val typeName: String, val path: String, + val defaultPenalty: WorkPenaltyType.WorkPenaltyPart, + val displayName: String, val displayMat: Material +) { + + RENAME_ONLY( + "rename_only", + WorkPenaltyType.WorkPenaltyPart(false, true), + "Rename Only", Material.NAME_TAG + ), + MERGE( + "merge", + WorkPenaltyType.WorkPenaltyPart(true, true), + "Merge", Material.ANVIL + ), + UNIT_REPAIR( + "unit_repair", + WorkPenaltyType.WorkPenaltyPart(true, true), + "Unit Repair", Material.DIAMOND + ), + CUSTOM_CRAFT( + "custom_craft", + WorkPenaltyType.WorkPenaltyPart(false, false), + "Custom Craft", Material.CRAFTING_TABLE + ), + LORE_EDIT_BOOK_APPEND( + "lore_edit_book_append", "lore_edit.book_and_quil.append", + WorkPenaltyType.WorkPenaltyPart(false, false), + "Book Add", Material.WRITABLE_BOOK + ), + LORE_EDIT_BOOK_REMOVE( + "lore_edit_book_remove", "lore_edit.book_and_quil.remove", + WorkPenaltyType.WorkPenaltyPart(false, false), + "Book Remove", Material.WRITABLE_BOOK + ), + LORE_EDIT_PAPER_APPEND( + "lore_edit_paper_append", "lore_edit.paper.append_line", + WorkPenaltyType.WorkPenaltyPart(false, false), + "Paper Add", Material.WRITABLE_BOOK + ), + LORE_EDIT_PAPER_REMOVE( + "lore_edit_paper_remove", "lore_edit.paper.remove_line", + WorkPenaltyType.WorkPenaltyPart(false, false), + "Paper Remove", Material.WRITABLE_BOOK + ), + ; + + constructor( + typeName: String, + defaultPenalty: WorkPenaltyType.WorkPenaltyPart, + displayName: String, displayMat: Material + ) : + this( + typeName, + AnvilUseTypeUtil.defaultPath(typeName), // stupid util class + defaultPenalty, + displayName, displayMat + ) + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt new file mode 100644 index 0000000..4c71c68 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt @@ -0,0 +1,46 @@ +package xyz.alexcrea.cuanvil.command + +import org.bukkit.ChatColor +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender + +abstract class CASubCommand: CommandExecutor { + + private var alreadySaid = false; + override fun onCommand( + sender: CommandSender, + cmd: Command, + cmdstr: String, + args: Array + ): Boolean { + if(!alreadySaid){ + sender.sendMessage(ChatColor.RED.toString() + + "Please not that this command will be replaced as a subcommand of `/customanvil` or `/ca`") + alreadySaid = true + } + + return executeCommand(sender, cmd, cmdstr, args) + } + + abstract fun executeCommand( + sender: CommandSender, + cmd: Command, + cmdstr: String, + args: Array): Boolean + + open fun allowed(sender: CommandSender): Boolean { + return true + } + + open fun tabCompleter( + sender: CommandSender, + args: Array, + list: MutableList) { + } + + open fun description(): String { + return "no description" + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCommand.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCommand.kt new file mode 100644 index 0000000..e5e689e --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCommand.kt @@ -0,0 +1,96 @@ +package xyz.alexcrea.cuanvil.command + +import com.google.common.collect.ImmutableMap +import io.delilaheve.CustomAnvil +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.command.TabCompleter +import xyz.alexcrea.cuanvil.util.MetricsUtil +import java.util.ArrayList + +class CustomAnvilCommand(plugin: CustomAnvil) : CommandExecutor, TabCompleter { + + // Name of the generic command + companion object { + private const val genericCommandName = "customanvil" + } + + private val editConfigCommand = EditConfigExecutor() + private val helpCommand = HelpExecutor() + private val commands = ImmutableMap.of( + "gui", editConfigCommand, + "reload", ReloadExecutor(), + "diagnostic", DiagnosticExecutor(), + "help", helpCommand, + ) + + init { + val self = plugin.getCommand(genericCommandName)!! + self.setExecutor(this) + self.tabCompleter = this + + helpCommand.commands = commands + } + + override fun onCommand( + sender: CommandSender, + cmd: Command, + cmdstr: String, + args: Array + ): Boolean { + // Find sub command to execute based on the provided command name + val subcmd: CASubCommand? + val newargs: Array + if(args.isEmpty()) { + subcmd = editConfigCommand + newargs = args + }else { + subcmd = commands[args[0].lowercase()] + newargs = args.copyOfRange(1, args.size) + } + + if(subcmd == null || !subcmd.allowed(sender)) { + sender.sendMessage("Invalid subcommand. run `$cmdstr help` to see available commands") + return true + } + + try { + return subcmd.executeCommand(sender, cmd, cmdstr, newargs) + } catch (e: Throwable) { + MetricsUtil.trackError(e) + sender.sendMessage("§cError running this command") + return false + } + } + + override fun onTabComplete( + sender: CommandSender, + cmd: Command, + cmdstr: String, + args: Array + ): MutableList { + val result = ArrayList() + if(args.size < 2) { + for ((key, cmd) in commands) { + if(!cmd.allowed(sender)) continue + result.add(key) + } + } else { + val subcmd = commands[args[0].lowercase()] + + if(subcmd != null) { + val newArgs = args.copyOfRange(1, args.size) + if(!subcmd.allowed(sender)) return result + + subcmd.tabCompleter(sender, newArgs, result) + } + } + + //assumed all provided tab completed string are lowercase + return result.stream() + .filter { it.startsWith(args[args.size - 1]) } + .sorted() + .toList() + } +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt new file mode 100644 index 0000000..053a3fc --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt @@ -0,0 +1,347 @@ +package xyz.alexcrea.cuanvil.command + +import com.github.stefvanschie.inventoryframework.inventoryview.interface_.InventoryViewUtil +import io.delilaheve.CustomAnvil +import net.md_5.bungee.api.chat.ClickEvent +import net.md_5.bungee.api.chat.HoverEvent +import net.md_5.bungee.api.chat.TextComponent +import net.md_5.bungee.api.chat.hover.content.Text +import org.bukkit.Bukkit +import org.bukkit.ChatColor +import org.bukkit.Material +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.HumanEntity +import org.bukkit.entity.Player +import org.bukkit.event.inventory.InventoryType +import org.bukkit.event.inventory.PrepareAnvilEvent +import org.bukkit.inventory.AnvilInventory +import org.bukkit.inventory.InventoryView +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.Damageable +import org.bukkit.inventory.meta.EnchantmentStorageMeta +import org.bukkit.plugin.Plugin +import org.bukkit.plugin.RegisteredListener +import xyz.alexcrea.cuanvil.dependency.DependencyManager +import xyz.alexcrea.cuanvil.dependency.packet.NoPacketManager +import xyz.alexcrea.cuanvil.dependency.packet.ProtocoLibWrapper +import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener +import xyz.alexcrea.cuanvil.util.MetricsUtil +import java.util.* +import java.util.stream.Collectors + + +class DiagnosticExecutor: CASubCommand() { + + companion object{ + private const val NO_DIAG_PERM = "You do not have permission to diagnostic this server" + + fun fetchNMSType(): String { + val packetManager = DependencyManager.packetManager + val packetManagerClass = packetManager.javaClass + + val className = packetManagerClass.name + val result = if(className.contains("PaperPacket")) { + "Paper" + } else { + when (packetManagerClass) { + ProtocoLibWrapper::class.java -> "Protocolib" + NoPacketManager::class.java -> "None" + else -> "Version Specific" + } + } + + + return "$result ${if(packetManager.canSetInstantBuild) '✅' else '❌'}" + } + } + + enum class DiagParams(val value: String) { + OS_PRIVACY("os_privacy"), + PLUGIN_PRIVACY("plugin_privacy"), + NO_MERGE_TEST("no_merge_test"), + FULL_ENCHANTMENT_DATA("full_enchantment_data"), + INCLUDE_LAST_ERROR("include_last_error"), + } + + private fun fetchParameters(args: Array): EnumSet { + val result = EnumSet.noneOf(DiagParams::class.java) + val argSet = HashSet() + + for (string in args) { + argSet.add(string.lowercase()) + } + + for (param in DiagParams.entries) { + if(argSet.contains(param.value)) + result.add(param) + } + + return result + } + + override fun tabCompleter( + sender: CommandSender, + args: Array, + list: MutableList) { + if(!allowed(sender)) return + + val map = fetchParameters(args) + for (param in DiagParams.entries) { + if(!map.contains(param)) + list.add(param.value) + } + + } + + override fun executeCommand( + sender: CommandSender, + cmd: Command, + cmdstr: String, + args: Array + ): Boolean { + if (!allowed(sender)) { + sender.sendMessage(NO_DIAG_PERM) + return false + } + + val stb = StringBuilder("```\n") + val params = fetchParameters(args) + var hasError = false + try { + diagnostic(sender, stb, params) + } catch(e: Throwable){ + stb.append("\n\nError happened trying to get diagnostic data:\n") + .append(e.message).append("\n") + .append(e.stackTrace.joinToString("\n")) + hasError = true + e.printStackTrace() + } + + stb.append("\n```") + + if (sender is HumanEntity) { + if(hasError) + sender.spigot().sendMessage(TextComponent(ChatColor.RED.toString() + "There was an error running the diagnostic")) + val message = TextComponent(ChatColor.GREEN.toString() + "Click to copy diagnostic data") + + message.clickEvent = ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, stb.toString()) + message.hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT, Text("§7Click to copy")) + + sender.spigot().sendMessage(message); + } else { + sender.sendMessage(stb.toString()); + } + + return true + } + + override fun allowed(sender: CommandSender): Boolean { + return sender.hasPermission(CustomAnvil.diagnosticPermission) + } + + fun diagnostic(sender: CommandSender, stb: StringBuilder, params: Set){ + stb.append("Server Info\n") + val version = CustomAnvil.instance.description.version + stb.append("\nPlugin Version: ").append(version) + if(version.contains("dev")) stb.append(" (alpha)") + + stb.append("\nLatest Update: ").append(CustomAnvil.latestVer) + stb.append("\nServer Version: ").append(Bukkit.getVersion()).append(" (").append(Bukkit.getName()).append(')') + stb.append("\nPlugin Enabled: ").append(if(CustomAnvil.instance.isEnabled) "Yes" else "No") + stb.append("\nNMS type: ").append(fetchNMSType()) + if(!params.contains(DiagParams.OS_PRIVACY)) { + stb.append("\nJava Version: ").append(System.getProperty("java.version")) + stb.append("\nOS: ").append(System.getProperty("os.name")).append(" ") + .append(System.getProperty("os.version")) + .append(System.getProperty("os.arch")) + } + + stb.append("\nHad detect error: ").append(if(MetricsUtil.lastError != null) "Yes" else "No") + + if(!params.contains(DiagParams.PLUGIN_PRIVACY)) { + pluginListDiag(sender, stb) + } + prepareAnvilListeners(stb) + + if(!params.contains(DiagParams.NO_MERGE_TEST)){ + if(sender is Player) testMerge(sender, stb) + } + + stb.append("\n\nEnchantments data:") + partialEnchantmentData(stb) + if(params.contains(DiagParams.FULL_ENCHANTMENT_DATA)){ + fullEnchantmentData(stb) + } + + if(params.contains(DiagParams.INCLUDE_LAST_ERROR)){ + includeLastError(stb) + } + } + + private fun testMerge(player: Player, stb: StringBuilder) { + val sword = ItemStack(Material.DIAMOND_SWORD) + val damagedSword = sword.clone() + val enchantedSword = sword.clone() + val enchantedBook = ItemStack(Material.ENCHANTED_BOOK) + val unitForRepair = ItemStack(Material.DIAMOND) + + var meta = damagedSword.itemMeta + (meta as Damageable).damage = 5 + damagedSword.itemMeta = meta + + meta = enchantedSword.itemMeta + meta!!.addEnchant(Enchantment.DAMAGE_ALL, 1, true) + enchantedSword.itemMeta = meta + + meta = enchantedBook.itemMeta + (meta as EnchantmentStorageMeta).addStoredEnchant(Enchantment.DAMAGE_ALL, 1, true) + enchantedBook.itemMeta = meta + + stb.append("\n\nItem to Item repair:") + simulateAnvil(player, stb, damagedSword, damagedSword, sword) + + stb.append("\n\nUnit repair:") + simulateAnvil(player, stb, damagedSword, unitForRepair, sword) + + stb.append("\n\nEnchanting an item:") + simulateAnvil(player, stb, sword, enchantedBook, enchantedSword) + } + + private val Plugin.pluginNameDisplay: String + get() { + return this.name + " v" + this.description.version + } + + override fun description(): String { + return "Basic diagnostic of this plugin" + } + + private fun pluginListDiag(sender: CommandSender, stb: StringBuilder) { + val enabledPlugins: MutableList = ArrayList() + val disabledPlugins: MutableList = ArrayList() + for (plugin in Bukkit.getPluginManager().plugins) { + if (plugin.isEnabled) { + enabledPlugins.add(plugin) + } else { + disabledPlugins.add(plugin) + } + } + + stb.append("\nEnabled Plugins: ").append( + enabledPlugins.stream() + .map { plugin -> plugin!!.pluginNameDisplay } + .reduce { a: String?, b: String? -> "$a, $b" }.orElse("None") + ) + + stb.append("\nDisabled Plugins: ").append( + disabledPlugins.stream() + .map { plugin -> plugin!!.pluginNameDisplay } + .reduce { a: String?, b: String? -> "$a, $b" }.orElse("None") + ) + } + + fun prepareAnvilListeners(stb: StringBuilder) { + val eventListeners: MutableSet = Arrays + .stream( + PrepareAnvilEvent + .getHandlerList() + .getRegisteredListeners() + ) + .map { obj: RegisteredListener? -> obj!!.plugin } + .collect(Collectors.toSet()) + + eventListeners.remove(CustomAnvil.instance) + stb.append("\nPrepare Anvil Listeners: ").append( + if (eventListeners.isEmpty()) "None" else eventListeners.stream() + .map { plugin -> plugin!!.pluginNameDisplay } + .reduce { a: String?, b: String? -> "$a, $b" }.orElse("None") + ) + } + + fun simulateAnvil(player: Player, stb: StringBuilder, left: ItemStack?, right: ItemStack?, result: ItemStack?) { + var invView: InventoryView + var event: PrepareAnvilEvent + try { + val fakeInv = Bukkit.createInventory(player, InventoryType.ANVIL) + invView = player.openInventory(fakeInv)!! + event = PrepareAnvilEvent(invView, result) + } catch (e: Throwable) { + // Help + val menuTypeClazz = Class.forName("org.bukkit.inventory.MenuType") + val anvilTypeField = menuTypeClazz.getField("ANVIL") + val anvilType = anvilTypeField.get(null) + val createMethod = anvilType.javaClass.getMethod("create", HumanEntity::class.java) + invView = createMethod.invoke(anvilType, player) as InventoryView + + player.openInventory(invView) + + val anvilViewClass = Class.forName("org.bukkit.inventory.view.AnvilView") + val constructor = PrepareAnvilEvent::class.java.getConstructor(anvilViewClass, ItemStack::class.java) + event = constructor.newInstance(invView, result) + } + + val fakeInv = InventoryViewUtil.getInstance().getTopInventory(invView) as AnvilInventory + fakeInv.setItem(0, left) + fakeInv.setItem(1, right) + + val xp = fakeInv.repairCost + val maxXp = fakeInv.maximumRepairCost + val mergeResult = fakeInv.getItem(2) + stb.append("\n${if(result == mergeResult) "E" else "Une"}xpected Result") + + PrepareAnvilListener().anvilCombineCheck(event) + // Now we check if item and xp same + stb.append("\nXP/Max XP: ") + .append(if(fakeInv.repairCost == xp) "Correct" else "Incorrect") + .append("/") + .append(if(fakeInv.maximumRepairCost == maxXp) "Correct" else "Incorrect") + .append(" (${fakeInv.repairCost} $xp|${fakeInv.maximumRepairCost} $maxXp)") + .append("\nMerge result: ") + .append(if(fakeInv.getItem(2) == mergeResult) "Correct" else "Incorrect") + + PrepareAnvilListener.IS_EMPTY_TEST = true + Bukkit.getPluginManager().callEvent(event) + stb.append("\nNull result test: ") + .append(if(event.result == null) "Correct" else "Incorrect") + + fakeInv.setItem(0, null) + fakeInv.setItem(1, null) + fakeInv.setItem(2, null) + player.closeInventory() + } + + private fun fullEnchantmentData(stb: StringBuilder) { + for (enchantment in CAEnchantmentRegistry.getInstance().values()) { + stb.append("\n- ").append(enchantment.key.toString()) + .append(" ").append(enchantment.name) + .append(" ").append(enchantment.defaultMaxLevel()) + } + } + + private fun partialEnchantmentData(stb: StringBuilder) { + val map = HashMap() + for (enchant in CAEnchantmentRegistry.getInstance().values()) { + map[enchant.key.namespace] = map.getOrDefault(enchant.key.namespace, 0) + 1 + } + + stb.append("\nNamespaces: ${ + map.entries.stream() + .map { (key, value) -> "$key ($value)" } + .reduce { a, b -> "$a, $b" }.get() + }") + + } + + private fun includeLastError(stb: StringBuilder) { + val e = MetricsUtil.lastError ?: return + + stb.append("\n\nLast stack trace: ${e.stackTraceToString()}") + + + + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/EditConfigExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/EditConfigExecutor.kt new file mode 100644 index 0000000..fba89b7 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/EditConfigExecutor.kt @@ -0,0 +1,45 @@ +package xyz.alexcrea.cuanvil.command + +import io.delilaheve.CustomAnvil +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.entity.HumanEntity +import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil +import xyz.alexcrea.cuanvil.gui.config.MainConfigGui +import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions + +class EditConfigExecutor: CASubCommand() { + + override fun executeCommand(sender: CommandSender, + cmd: Command, + cmdstr: String, + args: Array): Boolean { + if (sender !is HumanEntity) return false + + if (!allowed(sender)) { + sender.sendMessage(GuiGlobalActions.NO_EDIT_PERM) + return false + } + if(PlatformUtil.isFolia){ + sender.sendMessage("§cIt look like you are using Folia. Sadly Custom Anvil do not support Config gui for Folia.") + sender.sendMessage("§eIt is may come in a future version.") + sender.sendMessage("") + sender.sendMessage("§eCurrently you need to edit manually the config or copy from another server (spigot or better)") + sender.sendMessage("§eThen /anvilconfigreload after config file is edited") + return false + } + + MainConfigGui.getInstance().show(sender) + + return true + } + + override fun allowed(sender: CommandSender): Boolean { + return sender.hasPermission(CustomAnvil.editConfigPermission) + } + + override fun description(): String { + return "Gui to edit the plugin's config" + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/HelpExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/HelpExecutor.kt new file mode 100644 index 0000000..697f1ee --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/HelpExecutor.kt @@ -0,0 +1,32 @@ +package xyz.alexcrea.cuanvil.command + +import com.google.common.collect.ImmutableMap +import org.bukkit.command.Command +import org.bukkit.command.CommandSender + +class HelpExecutor: CASubCommand() { + + lateinit var commands: ImmutableMap + + override fun executeCommand(sender: CommandSender, + cmd: Command, + cmdstr: String, + args: Array): Boolean { + + val stb = StringBuilder("List of available commands:") + for ((key, cmd) in commands) { + if(!cmd.allowed(sender)) continue + + stb.append("\n- $key: ").append(cmd.description()) + } + + sender.sendMessage(stb.toString()) + + return true + } + + override fun description(): String { + return "Help command" + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/ReloadExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/ReloadExecutor.kt new file mode 100644 index 0000000..ef23e7d --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/ReloadExecutor.kt @@ -0,0 +1,79 @@ +package xyz.alexcrea.cuanvil.command + +import io.delilaheve.CustomAnvil +import org.bukkit.Bukkit +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import xyz.alexcrea.cuanvil.api.event.CAConfigReadyEvent +import xyz.alexcrea.cuanvil.config.ConfigHolder +import xyz.alexcrea.cuanvil.dependency.DependencyManager +import xyz.alexcrea.cuanvil.gui.config.global.* +import xyz.alexcrea.cuanvil.update.UpdateHandler + +class ReloadExecutor : CASubCommand() { + + override fun executeCommand(sender: CommandSender, + cmd: Command, + cmdstr: String, + args: Array): Boolean { + if (!allowed(sender)) { + sender.sendMessage("§cYou do not have permission to reload the config") + return false + } + sender.sendMessage("§eReloading config...") + val hardfail = args.isNotEmpty() && ("hard".equals(args[0], true)) + val commandSuccess = commandBody(hardfail) + if (commandSuccess) { + sender.sendMessage("§aConfig reloaded !") + } else { + sender.sendMessage("§cConfig was not able to be reloaded...") + if (hardfail) { + sender.sendMessage("§4Hard fail, plugin disabled") + } + } + return commandSuccess + } + + override fun allowed(sender: CommandSender): Boolean { + return sender.hasPermission(CustomAnvil.commandReloadPermission) + } + + override fun description(): String { + return "Reload the configuration of this plugin" + } + + /** + * Execute the command, return true if success or false otherwise + */ + private fun commandBody(hardfail: Boolean): Boolean { + try { + if (!ConfigHolder.reloadAllFromDisk(hardfail)) return false + + // Then update all global gui containing value from config + BasicConfigGui.getInstance()?.updateGuiValues() + EnchantCostConfigGui.getInstance()?.updateGuiValues() + EnchantLimitConfigGui.getInstance()?.updateGuiValues() + + EnchantConflictGui.getCurrentInstance()?.reloadValues() + GroupConfigGui.getCurrentInstance()?.reloadValues() + UnitRepairConfigGui.getCurrentInstance()?.reloadValues() + CustomRecipeConfigGui.getCurrentInstance()?.reloadValues() + + // handle minecraft version update + UpdateHandler.handleMCVersionUpdate() + + // Handle dependency reload + DependencyManager.handleConfigReload() + + // Call event + val configReadyEvent = CAConfigReadyEvent() + Bukkit.getServer().pluginManager.callEvent(configReadyEvent) + + return true + } catch (e: Exception) { + e.printStackTrace() + return false + } + } +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt new file mode 100644 index 0000000..b730a0e --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt @@ -0,0 +1,331 @@ +package xyz.alexcrea.cuanvil.dependency + +import com.maddoxh.superEnchants.SuperEnchants +import io.delilaheve.CustomAnvil +import net.kyori.adventure.text.Component +import org.bukkit.Bukkit +import org.bukkit.ChatColor +import org.bukkit.command.CommandSender +import org.bukkit.entity.HumanEntity +import org.bukkit.entity.Player +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.PrepareAnvilEvent +import org.bukkit.inventory.AnvilInventory +import org.bukkit.inventory.Inventory +import org.bukkit.inventory.InventoryView +import org.bukkit.inventory.ItemStack +import xyz.alexcrea.cuanvil.anvil.AnvilCost +import xyz.alexcrea.cuanvil.anvil.AnvilUseType +import xyz.alexcrea.cuanvil.api.event.listener.CAClickResultBypassEvent +import xyz.alexcrea.cuanvil.api.event.listener.CAEarlyPreAnvilBypassEvent +import xyz.alexcrea.cuanvil.api.event.listener.CAPreAnvilBypassEvent +import xyz.alexcrea.cuanvil.api.event.listener.CATreatAnvilResult2Event +import xyz.alexcrea.cuanvil.config.ConfigHolder +import xyz.alexcrea.cuanvil.dependency.datapack.DataPackDependency +import xyz.alexcrea.cuanvil.dependency.gui.GenericExternGuiTester +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerSelector +import xyz.alexcrea.cuanvil.dependency.plugins.* +import xyz.alexcrea.cuanvil.dependency.scheduler.BukkitScheduler +import xyz.alexcrea.cuanvil.dependency.scheduler.FoliaScheduler +import xyz.alexcrea.cuanvil.dependency.scheduler.TaskScheduler +import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil +import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil.componentLore +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener.Companion.ANVIL_OUTPUT_SLOT +import xyz.alexcrea.cuanvil.util.MetricsUtil.trackError +import java.util.logging.Level + +object DependencyManager { + + lateinit var scheduler: TaskScheduler + lateinit var packetManager: PacketManager + var externGuiTester: GenericExternGuiTester = GenericExternGuiTester() + + var enchantmentSquaredCompatibility: EnchantmentSquaredDependency? = null + var ecoEnchantCompatibility: EcoEnchantDependency? = null + var excellentEnchantsCompatibility: ExcellentEnchantsDependency? = null + + var disenchantmentCompatibility: DisenchantmentDependency? = null + var havenBagsCompatibility: HavenBagsDependency? = null + + var axPlayerWarpsCompatibility: AxPlayerWarpsDependency? = null + + var itemsAdderCompatibility: ItemsAdderDependency? = null + + val genericDependencies = ArrayList() + + fun loadDependency() { + val pluginManager = Bukkit.getPluginManager() + + // Bukkit or Paper scheduler ? + scheduler = if (PlatformUtil.isFolia) { + CustomAnvil.instance.logger.info("Folia detected... Custom Anvil Folia support is experimental. issues are more likely to happens.") + + FoliaScheduler() + } else BukkitScheduler() + + // Packet Manager + val forceProtocolib = ConfigHolder.DEFAULT_CONFIG.config.getBoolean("force_protocolib", false) + packetManager = PacketManagerSelector.selectPacketManager(forceProtocolib) + + // Enchantment Squared dependency + if (pluginManager.isPluginEnabled("EnchantsSquared")) { + enchantmentSquaredCompatibility = EnchantmentSquaredDependency(pluginManager.getPlugin("EnchantsSquared")!!) + enchantmentSquaredCompatibility!!.disableAnvilListener() + } + + // EcoEnchants dependency + if (pluginManager.isPluginEnabled("EcoEnchants")) { + ecoEnchantCompatibility = EcoEnchantDependency(pluginManager.getPlugin("EcoEnchants")!!) + ecoEnchantCompatibility!!.disableAnvilListener() + } + + // Excellent Enchants dependency + if (pluginManager.isPluginEnabled("ExcellentEnchants")) { + excellentEnchantsCompatibility = ExcellentEnchantsDependency() + excellentEnchantsCompatibility!!.redirectListeners() + } + + // Disenchantment dependency + if (pluginManager.isPluginEnabled("Disenchantment")) { + disenchantmentCompatibility = DisenchantmentDependency() + disenchantmentCompatibility!!.redirectListeners() + } + + // HavenBags dependency + if (pluginManager.isPluginEnabled("HavenBags")) { + havenBagsCompatibility = HavenBagsDependency() + havenBagsCompatibility!!.redirectListeners() + } + + // AxPlayerWarps dependency + if (pluginManager.isPluginEnabled("AxPlayerWarps")) { + axPlayerWarpsCompatibility = AxPlayerWarpsDependency() + } + + if (pluginManager.isPluginEnabled("ItemsAdder")){ + val dependency = ItemsAdderDependency(pluginManager.getPlugin("ItemsAdder")!!) + itemsAdderCompatibility = dependency + genericDependencies.add(dependency) + } + + // "Generic" dependencies + if (pluginManager.isPluginEnabled("ToolStats")) + genericDependencies.add(ToolStatsDependency(pluginManager.getPlugin("ToolStats")!!)) + + if (pluginManager.isPluginEnabled("ItemsAdder")) + genericDependencies.add(GenericPluginDependency(pluginManager.getPlugin("ItemsAdder")!!)) + + if (pluginManager.isPluginEnabled("SuperEnchants")){ + val compatibility = SuperEnchantDependency(pluginManager.getPlugin("SuperEnchants")!! as SuperEnchants) + if(compatibility.registerEnchantments()) + genericDependencies.add(compatibility) + } + + for (dependency in genericDependencies) + dependency.redirectListeners() + + } + + fun handleCompatibilityConfig() { + enchantmentSquaredCompatibility?.registerPluginConfiguration() + + // datapacks + DataPackDependency.handleDatapackConfigs() + } + + fun registerEnchantments() { + enchantmentSquaredCompatibility?.registerEnchantments() + ecoEnchantCompatibility?.registerEnchantments() + excellentEnchantsCompatibility?.registerEnchantments() + + } + + fun handleConfigReload() { + // Register enchantment of compatible plugin and load configuration change. + handleCompatibilityConfig() + + // Then handle plugin reload + ecoEnchantCompatibility?.handleConfigReload() + } + + private fun logException(target: CommandSender, e: Exception) { + CustomAnvil.instance.logger.log( + Level.SEVERE, + "Error while trying to handle custom anvil supported plugin: ", + e + ) + trackError(e) + + // Finally, warn the player + target.sendMessage( + "[" + ChatColor.YELLOW.toString() + "CustomAnvil" + ChatColor.WHITE.toString() + "] " + + ChatColor.RED.toString() + "Error while handling the anvil." + ) + } + + private fun logExceptionAndClear(target: CommandSender, inventory: Inventory, e: Exception) { + // Just in case to avoid illegal items + inventory.setItem(ANVIL_OUTPUT_SLOT, null) + + logException(target, e) + } + + // Return true if should bypass (either by a dependency or error) + // called before immutability test + fun earlyTryEventPreAnvilBypass(event: PrepareAnvilEvent, player: HumanEntity): Boolean { + try { + return earlyUnsafeTryEventPreAnvilBypass(event, player) + } catch (e: Exception) { + logExceptionAndClear(event.view.player, event.inventory, e) + return true + } + } + + private fun earlyUnsafeTryEventPreAnvilBypass(event: PrepareAnvilEvent, player: HumanEntity): Boolean { + // Run the event + val bypassEvent = CAEarlyPreAnvilBypassEvent(event) + Bukkit.getPluginManager().callEvent(bypassEvent) + + var bypass = bypassEvent.isCancelled + + // Test if the inventory is a gui(version specific) + if (!bypass && externGuiTester.testIfGui(event.view)) bypass = true + + // Test if in an ax player warp rating gui + if (!bypass && (axPlayerWarpsCompatibility?.testIfGui(player) == true)) bypass = true + + return bypass + } + + // Return true if should bypass (either by a dependency or error) + fun tryEventPreAnvilBypass(event: PrepareAnvilEvent, player: Player): Boolean { + try { + return unsafeTryEventPreAnvilBypass(event, player) + } catch (e: Exception) { + logExceptionAndClear(event.view.player, event.inventory, e) + return true + } + } + + private fun unsafeTryEventPreAnvilBypass(event: PrepareAnvilEvent, player: Player): Boolean { + // Run the event + val bypassEvent = CAPreAnvilBypassEvent(event) + Bukkit.getPluginManager().callEvent(bypassEvent) + + var bypass = bypassEvent.isCancelled + + // Test if disenchantment used prepare anvil + if (!bypass && (disenchantmentCompatibility?.testPrepareAnvil(event, player) == true)) bypass = true + + // Test heaven bags used prepare anvil + if (!bypass && (havenBagsCompatibility?.testPrepareAnvil(event, player) == true)) bypass = true + + // Test excellent enchantments used prepare anvil + if (!bypass && (excellentEnchantsCompatibility?.testPrepareAnvil(event) == true)) bypass = true + + for (genericDependency in genericDependencies) { + if (!bypass && genericDependency.testPrepareAnvil(event)) bypass = true + } + + return bypass + } + + // Return null if there was an issue + fun tryTreatAnvilResult( + view: InventoryView, + inventory: Inventory, // TODO REMOVE, use view instead on legacy removal + player: HumanEntity, + result: ItemStack, + useType: AnvilUseType, + cost: AnvilCost + ): ItemStack? { + val treatEvent = CATreatAnvilResult2Event(view, inventory, useType, result, cost) + try { + unsafeTryTreatAnvilResult(treatEvent) + return treatEvent.result + } catch (e: Exception) { + logExceptionAndClear(player, inventory, e) + return null + } + } + + private fun unsafeTryTreatAnvilResult(event: CATreatAnvilResult2Event) { + Bukkit.getPluginManager().callEvent(event) + + excellentEnchantsCompatibility?.treatAnvilResult(event) + } + + // Return true if should bypass (either by a dependency or error) + fun tryClickAnvilResultBypass(event: InventoryClickEvent, inventory: AnvilInventory): Boolean { + try { + return unsafeTryClickAnvilResultBypass(event, inventory) + } catch (e: Exception) { + logExceptionAndClear(event.view.player, event.inventory, e) + return true + } + } + + private fun unsafeTryClickAnvilResultBypass(event: InventoryClickEvent, inventory: AnvilInventory): Boolean { + // Run the event + val bypassEvent = CAClickResultBypassEvent(event) + Bukkit.getPluginManager().callEvent(bypassEvent) + + var bypass = bypassEvent.isCancelled + + // Test if disenchantment used event click + if (!bypass && (disenchantmentCompatibility?.testAnvilResult(event, inventory) == true)) bypass = true + + // Test if haven bag used event click + if (!bypass && (havenBagsCompatibility?.testAnvilResult(event, inventory) == true)) bypass = true + + // Test if disenchantment used event click + if (!bypass && (excellentEnchantsCompatibility?.testAnvilResult(event) == true)) bypass = true + + for (genericDependency in genericDependencies) { + if (!bypass && genericDependency.testAnvilResult(event)) bypass = true + } + + // Test if the inventory is a gui(version specific) + if (!bypass && externGuiTester.testIfGui(event.view)) bypass = true + + // Test if in an ax player warp rating gui + if (!bypass && (axPlayerWarpsCompatibility?.testIfGui(event.view.player) == true)) bypass = true + + return bypass + } + + // Clone item and use plugin specific clone if needed + fun cloneItem(player: HumanEntity, item: ItemStack): ItemStack { + try { + return unsafeCloneItem(item) + } catch (e: Exception) { + logException(player, e) + return item.clone() + } + } + + private fun unsafeCloneItem(item: ItemStack): ItemStack { + val cloned = itemsAdderCompatibility?.tryClone(item) + if(cloned != null) return cloned + + return item.clone() + } + + fun stripLore(item: ItemStack): MutableList { + val dummy = item.clone() + + enchantmentSquaredCompatibility?.stripLore(dummy) + + val itemLore = dummy.itemMeta?.componentLore() ?: return ArrayList() + + val lore = ArrayList() + lore.addAll(itemLore) + return lore + } + + fun updateLore(item: ItemStack) { + enchantmentSquaredCompatibility?.updateLore(item) + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/MinecraftVersionUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/MinecraftVersionUtil.kt new file mode 100644 index 0000000..cbd2209 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/MinecraftVersionUtil.kt @@ -0,0 +1,58 @@ +package xyz.alexcrea.cuanvil.dependency + +import xyz.alexcrea.cuanvil.update.UpdateUtils + +object MinecraftVersionUtil { + + val craftbukkitVersion: String? + get() { + val version = UpdateUtils.currentMinecraftVersion() + if (version.major != 1) return null + + return when (version.minor) { + 17 -> when (version.patch) { + 0, 1 -> "1_17R1" + else -> null + } + + 18 -> when (version.patch) { + 0, 1 -> "1_18R1" + 2 -> "1_18R2" + else -> null + } + + 19 -> when (version.patch) { + 0, 1, 2 -> "1_19R1" + 3 -> "1_19R2" + 4 -> "1_19R3" + else -> null + } + + 20 -> when (version.patch) { + 0, 1 -> "1_20R1" + 2 -> "1_20R2" + 3, 4 -> "1_20R3" + 5, 6 -> "1_20R4" + else -> null + } + + 21 -> when (version.patch) { + 0, 1 -> "1_21R1" + 2, 3 -> "1_21R2" + 4 -> "1_21R3" + 5 -> "1_21R4" + 6, 7, 8 -> "1_21R5" + 9, 10 -> "1_21R6" + 11 -> "1_21R7" + else -> null + } + + else -> null + } + } + + val isTooNewForSpigot: Boolean get() { + return UpdateUtils.currentMinecraftVersion().major != 1 + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/datapack/DataPackDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/datapack/DataPackDependency.kt new file mode 100644 index 0000000..6d54456 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/datapack/DataPackDependency.kt @@ -0,0 +1,305 @@ +package xyz.alexcrea.cuanvil.dependency.datapack + +import io.delilaheve.CustomAnvil +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.configuration.file.FileConfiguration +import org.bukkit.configuration.file.YamlConfiguration +import xyz.alexcrea.cuanvil.api.ConflictBuilder +import xyz.alexcrea.cuanvil.api.EnchantmentApi +import xyz.alexcrea.cuanvil.api.MaterialGroupApi +import xyz.alexcrea.cuanvil.config.ConfigHolder +import xyz.alexcrea.cuanvil.enchant.wrapped.CABukkitEnchantment +import xyz.alexcrea.cuanvil.enchant.wrapped.CAIncompatibleAllEnchant +import xyz.alexcrea.cuanvil.group.IncludeGroup +import xyz.alexcrea.cuanvil.update.UpdateUtils +import xyz.alexcrea.cuanvil.update.Version +import java.io.InputStreamReader + +object DataPackDependency { + private val START_DETECT_VERSION = Version(1, 20, 5) + + /** + * Map of the latest CustomAnvil update related to the pack + */ + private val LASTEST_VERSION = mapOf( + Pair("bracken", Version(1, 11, 0)), + Pair("enchantplus", Version(1, 13, 0)), + Pair("dungeons_and_taverns", Version(1, 13, 0)) + ) + + val enabledDatapacks: List + get() { + val version: Version = UpdateUtils.currentMinecraftVersion() + if (version.lesserThan(START_DETECT_VERSION)) return emptyList() + + return DataPackTester.enabledPacks + } + + fun handleDatapackConfigs() { + val enabledDatapack = enabledDatapacks + for (packName in enabledDatapack) { + // Handling of pack name is horrible: it is based on file name + // So if someone rename a datapack it will make me sad + if(!packName.startsWith("file/")) continue + + if (packName.contains("bp_post_scarcity", ignoreCase = true) + || packName.contains("bracken", ignoreCase = true)) { + handlePack("bracken") + continue + } + + if (packName.contains("neoenchant", ignoreCase = true)) { + handlePack("enchantplus") + continue + } + + if (packName.contains("Dungeons and Taverns", ignoreCase = true)) { + handlePack("dungeons_and_taverns") + continue + } + + } + } + + private fun handlePack(pack: String){ + CustomAnvil.instance.logger.info("trying to handle datapack $pack") + handlePackInitialConfig(pack) + writeDefaultByNamespace(pack) + CustomAnvil.instance.logger.info("configuration done for $pack") + } + + private fun handlePackInitialConfig(pack: String) { + val defConfig = ConfigHolder.DEFAULT_CONFIG + val version = LASTEST_VERSION[pack] + if(version == null) { + throw RuntimeException("The pack $pack has no latest version hard coded in the plugin") + } + + val currentVersion = Version.fromString(defConfig.config.getString("datapack.$pack")) + if (currentVersion.greaterEqual(version)) { + handleEnchantAllConflict(pack) + return + } + + // Add pack value or do update from previous version + // note: update thingy is not yet implemented + configureDatapack(pack) + + // Finally, set current pack version to config + defConfig.config.set("datapack.$pack", version.toString()) + defConfig.saveToDisk(true) + } + + private fun configureDatapack(pack: String) { + val itemGroups = javaClass.getResource("/datapack/$pack/item_groups.yml") + val itemConflict = javaClass.getResource("/datapack/$pack/item_conflict.yml") + val enchantConflict = javaClass.getResource("/datapack/$pack/enchant_conflict.yml") + + if (itemGroups != null) { + val reader = InputStreamReader(itemGroups.openStream()) + val yml = YamlConfiguration.loadConfiguration(reader) + + handleItemGroups(yml) + } + + val newConflictList = ArrayList() + var needSave = false + if (itemConflict != null) { + val reader = InputStreamReader(itemConflict.openStream()) + val yml = YamlConfiguration.loadConfiguration(reader) + + addItemConflicts(yml, newConflictList) + } + + if (enchantConflict != null) { + val reader = InputStreamReader(enchantConflict.openStream()) + val yml = YamlConfiguration.loadConfiguration(reader) + + needSave = addEnchantConflict(yml, newConflictList) + } + + for (conflict in newConflictList) { + needSave = !conflict.registerIfAbsent() && needSave + } + + if (needSave) { + ConfigHolder.CONFLICT_HOLDER.saveToDisk(true) + } + } + + // Order matter for this file + // Could rewrite to not matter but not really important, so I keep it like that + private fun handleItemGroups(yml: YamlConfiguration) { + for (groupName in yml.getKeys(false)) { + val section = yml.getConfigurationSection(groupName) ?: continue + + var group = MaterialGroupApi.getGroup(groupName) + val exist = group != null + + if (group == null) group = IncludeGroup(groupName) + + for (name in section.getStringList("items")) { + val mat = Material.getMaterial(name.uppercase()) + if (mat == null) { + CustomAnvil.instance.logger.warning("Could not find material $name for item group $groupName") + continue + } + group.addToPolicy(mat.key) + } + for (name in section.getStringList("groups")) { + val otherGroup = MaterialGroupApi.getGroup(name) + if (otherGroup == null) { + CustomAnvil.instance.logger.warning("Could not find sub group $name for group $groupName") + continue + } + + group.addToPolicy(otherGroup) + } + + group.updateMaterials() + + if (exist) { + MaterialGroupApi.writeMaterialGroup(group) + } else { + MaterialGroupApi.addMaterialGroup(group, true) + } + } + } + + private fun addItemConflicts(yml: FileConfiguration, conflictList: MutableList) { + for (ench in yml.getKeys(false)) { + val groups = yml.getStringList(ench) + + val conflict = ConflictBuilder( + "restriction_${ench.replace(":", "_")}", + CustomAnvil.instance + ) + conflict.addEnchantment(NamespacedKey.fromString(ench)!!) + for (group in groups) { + conflict.addExcludedGroup(group) + } + conflict.addExcludedGroup("enchanted_book") + + conflictList.add(conflict) + } + } + + private fun addEnchantConflict(yml: YamlConfiguration, conflictList: MutableList): Boolean { + var needSave = false + + val conflicts = HashMap() + for (ench in yml.getKeys(false)) { + val groups = yml.getStringList(ench) + + for (group in groups) { + if (group.startsWith('#')) { + needSave = joinGroup(conflicts, group.substring(1), ench) || needSave + } else { + createConflict(conflictList, ench, group) + } + } + } + + conflictList.addAll(conflicts.values) + return needSave + } + + private fun createConflict(conflictList: MutableList, ench: String, other: String) { + + val conflict = ConflictBuilder( + "conflict_" + + "${ench.replace(":", "_")}_" + + other.replace(":", "_"), + CustomAnvil.instance + ) + conflict.addEnchantment(NamespacedKey.fromString(ench)!!) + conflict.addEnchantment(NamespacedKey.fromString(other)!!) + + conflict.setMaxBeforeConflict(1) + + conflictList.add(conflict) + } + + private fun setEnchantAsAll(ench: String) { + // We assume current is not null and of type CABukkitEnchantment + val current = EnchantmentApi.getByKey(NamespacedKey.fromString(ench)!!) as CABukkitEnchantment + + // We need to replace current wrapped enchantment with the all conflict wrapper + EnchantmentApi.unregisterEnchantment(current) + EnchantmentApi.registerEnchantment(CAIncompatibleAllEnchant(current.bukkit, current.defaultRarity())) + } + + private fun joinGroup(conflicts: HashMap, group: String, ench: String): Boolean { + if ("all".equals(group, ignoreCase = true)) { + setEnchantAsAll(ench) + return false + } else { + val config = ConfigHolder.CONFLICT_HOLDER.config + + // If conflict do not yet exist + if (!config.isConfigurationSection(group)) { + val conflict = conflicts.getOrPut(group) { + val conflict = ConflictBuilder(group, CustomAnvil.instance) + conflict.setMaxBeforeConflict(1) + conflict + } + + conflict.addEnchantment(NamespacedKey.fromString(ench)!!) + return false + } + // Find current conflict + val manager = ConfigHolder.CONFLICT_HOLDER.conflictManager + + // This assumes that: + // - the conflict existing in the config exist in the runtime config (as configuration section exist) + // - the enchantment exist and is provided correctly + val conflict = manager.conflictList.find { + it.name.equals(group, ignoreCase = true) + } + if(conflict == null) { + // This should not happen as configuration section + CustomAnvil.instance.logger.severe("Could not find $group while its configuration section exist... this should NOT happen") + return false + } + + val key = NamespacedKey.fromString(ench)!! + val enchant = EnchantmentApi.getByKey(key) + if (enchant == null){ + CustomAnvil.instance.logger.severe("Could not find enchantment $ench while configuring pack a datapack") + return false + } + + conflict.addEnchantment(enchant) + + UpdateUtils.addAbsentToList(config, "$group.enchantments", ench) + return true + } + } + + private fun handleEnchantAllConflict(pack: String) { + val enchantConflict = javaClass.getResource("/datapack/$pack/enchant_conflict.yml") ?: return + + val reader = InputStreamReader(enchantConflict.openStream()) + val yml = YamlConfiguration.loadConfiguration(reader) + + for (ench in yml.getKeys(false)) { + val groups = yml.getStringList(ench) + + if (groups.contains("#all")) { + setEnchantAsAll(ench) + } + } + } + + private fun writeDefaultByNamespace(namespace: String) { + for (enchantment in EnchantmentApi.getRegisteredEnchantments().values) { + if(!enchantment.key.namespace.equals(namespace, ignoreCase = true)) continue + + CustomAnvil.log("Writing default for ${enchantment.key}") + EnchantmentApi.writeDefaultConfig(enchantment, false) + } + + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/EconomyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/EconomyManager.kt new file mode 100644 index 0000000..67a6c1e --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/EconomyManager.kt @@ -0,0 +1,32 @@ +package xyz.alexcrea.cuanvil.dependency.economy + +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin +import java.math.BigDecimal + +interface EconomyManager { + + companion object { + var economy: EconomyManager? = null + + fun setupEconomy(plugin: Plugin) { + if (plugin.server.pluginManager.getPlugin("Vault") == null) + return + if (UnlockedEconomyManager.unlockedAvailable()) + economy = UnlockedEconomyManager(plugin) + + if (economy == null || !economy!!.initialized()) + economy = VaultEconomyManager(plugin) + } + + } + + fun initialized(): Boolean + + // We assume "initialized" got checked before these function get called + fun has(player: Player, money: BigDecimal): Boolean + fun remove(player: Player, money: BigDecimal): Boolean + + fun format(money: BigDecimal): String; + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/UnlockedEconomyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/UnlockedEconomyManager.kt new file mode 100644 index 0000000..da66f30 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/UnlockedEconomyManager.kt @@ -0,0 +1,74 @@ +package xyz.alexcrea.cuanvil.dependency.economy + +import io.delilaheve.util.ConfigOptions +import net.milkbowl.vault2.economy.Economy +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin +import java.math.BigDecimal + +class UnlockedEconomyManager : EconomyManager { + + val plugin: String + val economy: Economy? + + companion object { + fun unlockedAvailable(): Boolean { + try { + Class.forName("net.milkbowl.vault2.economy.Economy") + return true + } catch (_: ClassNotFoundException) { + return false + } + } + } + + constructor(plugin: Plugin) { + this.plugin = plugin.name + + val rsp = plugin.server.servicesManager.getRegistration(Economy::class.java) + economy = rsp?.getProvider() + } + + override fun initialized(): Boolean { + return economy != null + } + + private fun currency(): String { + val configured = ConfigOptions.usedCurrency + + return if ("default".equals(configured, true)) + economy!!.getDefaultCurrency(plugin) + else configured + } + + override fun has(player: Player, money: BigDecimal): Boolean { + if (money.signum() <= 0) return true + + return economy!!.has( + plugin, + player.uniqueId, + player.world.name, + currency(), + money + ) + } + + override fun remove(player: Player, money: BigDecimal): Boolean { + if (money.signum() <= 0) return true + + return economy!!.withdraw( + plugin, + player.uniqueId, + player.world.name, + currency(), + money + ) + .transactionSuccess() + } + + override fun format(money: BigDecimal): String { + return economy!!.format(plugin, money, currency()) + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/VaultEconomyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/VaultEconomyManager.kt new file mode 100644 index 0000000..058485e --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/VaultEconomyManager.kt @@ -0,0 +1,37 @@ +package xyz.alexcrea.cuanvil.dependency.economy + +import net.milkbowl.vault.economy.Economy +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin +import java.math.BigDecimal + +class VaultEconomyManager : EconomyManager { + + val economy: Economy? + + constructor(plugin: Plugin) { + val rsp = plugin.server.servicesManager.getRegistration(Economy::class.java) + economy = rsp?.getProvider() + } + + override fun initialized(): Boolean { + return economy != null + } + + override fun has(player: Player, money: BigDecimal): Boolean { + if (money.signum() <= 0) return true + + return economy!!.has(player, money.toDouble()) + } + + override fun remove(player: Player, money: BigDecimal): Boolean { + if (money.signum() <= 0) return true + + return economy!!.withdrawPlayer(player, money.toDouble()).transactionSuccess() + } + + override fun format(money: BigDecimal): String { + return economy!!.format(money.toDouble()) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GenericExternGuiTester.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GenericExternGuiTester.kt new file mode 100644 index 0000000..4046f4a --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GenericExternGuiTester.kt @@ -0,0 +1,111 @@ +package xyz.alexcrea.cuanvil.dependency.gui + +import org.bukkit.inventory.InventoryView +import xyz.alexcrea.cuanvil.dependency.MinecraftVersionUtil +import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil +import java.lang.reflect.Method + +class GenericExternGuiTester { + + companion object { + private const val ANVIL_CLASS_NAME = "org.bukkit.craftbukkit.inventory.view.CraftAnvilView" + private const val INV_CLASS_NAME = "org.bukkit.craftbukkit.inventory.CraftInventoryView" + private const val HANDLE_METHOD_NAME = "getHandle" + + private const val CANONICAL_PAPER_ANVIL_MENU = "net.minecraft.world.inventory.AnvilMenu" + } + + var testExist = false + var inTesting = false + + var testedClass: String? = null + lateinit var getHandleMethod: Method + + private fun getContainerClass(view: InventoryView): Class? { + if(!testedClass.contentEquals(view.javaClass.name)) + return null + + val container = getHandleMethod.invoke(view) + return container.javaClass + } + + fun tryFromClass(className: String) { + val clazz = Class.forName(className) + testedClass = className + + getHandleMethod = clazz.getMethod(HANDLE_METHOD_NAME) + } + + fun isInTest(): Boolean { + if(!testExist) testClassExist() + return inTesting + } + + fun testClassExist() { + testExist = true + + // We first try to get craft anvil interface, + // but is absent on old version so we try craft inventory view before + try { + tryFromClass(ANVIL_CLASS_NAME) + return + } + catch (_: ClassNotFoundException) {} + catch (_: NoSuchMethodException) {} + + try { + tryFromClass(INV_CLASS_NAME) + return + } + catch (_: ClassNotFoundException) {} + catch (_: NoSuchMethodException) {} + + inTesting = true + } + + // Try if were in another plugin anvil inventory + fun testIfGui(inventory: InventoryView): Boolean { + // In case we are in a test environment + if(isInTest()) return false + + val clazz = getContainerClass(inventory) ?: return false + + val clazzName = clazz.name + if(!PlatformUtil.isPaper){ + // Blacklist gui causing issue + if (expectWesjd(clazzName)) return true + if (expectXenondevUI(clazzName)) return true + if (expectVanePortal(clazzName)) return true + + return false + } + + // Only allow cannonical anvil menu class + return !CANONICAL_PAPER_ANVIL_MENU.equals(clazzName, true) + } + + // Known custom implementations + fun expectWesjd(name: String): Boolean { + val expectedWesjdGuiPath = "anvilgui.version.Wrapper${MinecraftVersionUtil.craftbukkitVersion}" + + return name.contains(expectedWesjdGuiPath) + } + + private val XenondevUIPrefix: String + get() = "xyz.xenondevs.inventoryaccess." + private val XenondevUISufix: String + get() = ".AnvilInventoryImpl" + + fun expectXenondevUI(name: String): Boolean { + return name.startsWith(XenondevUIPrefix) + && name.endsWith(XenondevUISufix) + } + + fun expectVanePortal(name: String): Boolean { + val expected = "org.oddlama.vane.core.menu.AnvilMenu\$AnvilContainer" + + return name == expected + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerSelector.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerSelector.kt new file mode 100644 index 0000000..d74ef08 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerSelector.kt @@ -0,0 +1,54 @@ +package xyz.alexcrea.cuanvil.dependency.packet + +import org.bukkit.Bukkit +import xyz.alexcrea.cuanvil.dependency.DependencyManager +import xyz.alexcrea.cuanvil.dependency.MinecraftVersionUtil +import xyz.alexcrea.cuanvil.dependency.packet.versions.* +import xyz.alexcrea.cuanvil.update.UpdateUtils + +object PacketManagerSelector { + + private const val PAPER_CRAFT_PLAYER_CLASS = "org.bukkit.craftbukkit.entity.CraftPlayer" + + fun selectPacketManager(forceProtocolib: Boolean): PacketManager { + // Try to find version + if(DependencyManager.externGuiTester.isInTest()) + return NoPacketManager() + + return if (forceProtocolib) + protocolibIfPresent + else { + try { + Class.forName(PAPER_CRAFT_PLAYER_CLASS) + + return PaperPacketManager() + } catch (_: ClassNotFoundException) { + return reobfPacketManager ?: protocolibIfPresent + } + } + } + + private val protocolibIfPresent: PacketManager + get() = + if (Bukkit.getPluginManager().isPluginEnabled("ProtocolLib")) + ProtocoLibWrapper() + else + NoPacketManager() + + // Reobfuscated packet manager for spigot or paper as it remap + private val reobfPacketManager: PacketManagerBase? + get() { + val versionParts = UpdateUtils.currentMinecraftVersion() + if (versionParts.major != 1) return null + + try { + val clazz = Class.forName("xyz.alexcrea.cuanvil.dependency.packet.versions." + + "V${MinecraftVersionUtil.craftbukkitVersion}_PacketManager") + + val manager = clazz.getConstructor().newInstance() + return manager as PacketManagerBase + } catch (_: ClassNotFoundException) { + return null + } + } +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/AxPlayerWarpsDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/AxPlayerWarpsDependency.kt new file mode 100644 index 0000000..e0ca218 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/AxPlayerWarpsDependency.kt @@ -0,0 +1,13 @@ +package xyz.alexcrea.cuanvil.dependency.plugins + +import com.artillexstudios.axplayerwarps.libs.axapi.gui.AnvilInput +import org.bukkit.entity.HumanEntity +import org.bukkit.entity.Player + +class AxPlayerWarpsDependency { + + fun testIfGui(player: HumanEntity): Boolean { + return player is Player && AnvilInput.get(player) != null + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt new file mode 100644 index 0000000..690b384 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt @@ -0,0 +1,102 @@ +package xyz.alexcrea.cuanvil.dependency.plugins + +import com.jankominek.disenchantment.Disenchantment +import com.jankominek.disenchantment.events.DisenchantClickEvent +import com.jankominek.disenchantment.events.DisenchantEvent +import com.jankominek.disenchantment.events.ShatterClickEvent +import com.jankominek.disenchantment.events.ShatterEvent +import com.jankominek.disenchantment.listeners.DisenchantClickListener +import com.jankominek.disenchantment.listeners.ShatterClickListener +import io.delilaheve.CustomAnvil +import org.bukkit.entity.Player +import org.bukkit.event.Listener +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.PrepareAnvilEvent +import org.bukkit.inventory.AnvilInventory +import org.bukkit.inventory.ItemStack +import xyz.alexcrea.cuanvil.anvil.AnvilCost +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener +import xyz.alexcrea.cuanvil.util.MetricsUtil.trackError +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil +import java.util.logging.Level +import kotlin.reflect.KClass + +class DisenchantmentDependency { + + init { + CustomAnvil.instance.logger.info("Disenchantment Detected !") + } + + fun redirectListeners() { + PrepareAnvilEvent.getHandlerList().unregister(Disenchantment.plugin) + + // unregister only the feature click event and not all + // This is to avoid the disenchantment gui breaking + try { + unregisterStaticDisenchantmentListener(ShatterClickListener::class) + unregisterStaticDisenchantmentListener(DisenchantClickListener::class) + } catch (e: Exception) { + CustomAnvil.instance.logger.log( + Level.SEVERE, "Could not initialize disenchantment support" + + "please report this bug to the developer", e + ) + trackError(e) + } + } + + private fun unregisterStaticDisenchantmentListener(clazz: KClass<*>) { + val field = clazz.java.getDeclaredField("listener") + field.isAccessible = true + val listener: Listener = field.get(null) as Listener + InventoryClickEvent.getHandlerList().unregister(listener) + } + + fun testPrepareAnvil(event: PrepareAnvilEvent, player: Player): Boolean { + val previousResult = event.result + event.result = null + + // Test if event change the result + DisenchantEvent.onEvent(event) + if (event.result != null) { + CustomAnvil.log("Detected pre anvil item extract bypass.") + AnvilXpUtil.setAnvilInvCost(event.inventory, event.view, player, AnvilCost(event.inventory.repairCost)) + return true + } + + ShatterEvent.onEvent(event) + if (event.result != null) { + CustomAnvil.log("Detected pre anvil split enchant bypass.") + AnvilXpUtil.setAnvilInvCost(event.inventory, event.view, player, AnvilCost(event.inventory.repairCost)) + return true + } + + event.result = previousResult + return false + } + + fun testAnvilResult(event: InventoryClickEvent, inventory: AnvilInventory): Boolean { + val previousResultSlot = inventory.getItem(PrepareAnvilListener.ANVIL_OUTPUT_SLOT)?.clone() + + // Test event if change the result + DisenchantClickEvent.onEvent(event) + if (!testAnvilInventoryChange(inventory, previousResultSlot) || event.isCancelled) { + CustomAnvil.log("Detected anvil click item extract bypass.") + return true + } + + ShatterClickEvent.onEvent(event) + if (!testAnvilInventoryChange(inventory, previousResultSlot) || event.isCancelled) { + CustomAnvil.log("Detected anvil click split enchant bypass.") + return true + } + + return false + } + + private fun testAnvilInventoryChange(inventory: AnvilInventory, previous: ItemStack?): Boolean { + val currentResult = inventory.getItem(PrepareAnvilListener.ANVIL_OUTPUT_SLOT) + + return currentResult == previous + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/EcoEnchantDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/EcoEnchantDependency.kt new file mode 100644 index 0000000..079d570 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/EcoEnchantDependency.kt @@ -0,0 +1,90 @@ +package xyz.alexcrea.cuanvil.dependency.plugins + +import com.willfp.eco.core.EcoPlugin +import com.willfp.ecoenchants.enchant.EcoEnchant +import com.willfp.ecoenchants.enchant.EcoEnchants +import com.willfp.ecoenchants.mechanics.infiniteIfNegative +import io.delilaheve.CustomAnvil +import org.bukkit.event.inventory.PrepareAnvilEvent +import org.bukkit.plugin.Plugin +import xyz.alexcrea.cuanvil.api.EnchantmentApi +import xyz.alexcrea.cuanvil.enchant.wrapped.CAEcoEnchant + +class EcoEnchantDependency(private val ecoEnchantPlugin: Plugin) { + + private val isLegacy: Boolean + private val legacyDependency: LegacyEcoEnchantDependency? + + init { + CustomAnvil.instance.logger.info("Eco Enchant Detected !") + + var isLegacy = true + try { + Class.forName("com.willfp.ecoenchants.enchant.EcoEnchants") + isLegacy = false + } catch (_: ClassNotFoundException) { + } + + this.isLegacy = isLegacy; + if (isLegacy) { + this.legacyDependency = LegacyEcoEnchantDependency() + } else { + this.legacyDependency = null + } + + } + + public fun getEcoLevelLimit(): Int { + return (ecoEnchantPlugin as EcoPlugin).configYml.getInt("anvil.enchant-limit").infiniteIfNegative() + } + + fun disableAnvilListener() { + PrepareAnvilEvent.getHandlerList().unregister(this.ecoEnchantPlugin) + } + + private var ecoEnchantOldEnchantments: MutableSet? = null + fun registerEnchantments() { + CustomAnvil.instance.logger.info("Preparing Eco Enchant compatibility...") + + if (isLegacy) { + legacyDependency!!.registerEnchantments(); + return + } + + val enchantments = EcoEnchants.values() + for (ecoEnchant in enchantments) { + EnchantmentApi.unregisterEnchantment(ecoEnchant.enchantment) // As eco enchants is loaded before custom anvil and register enchantment to registry, we need to unregister old "vanilla" enchant. + EnchantmentApi.registerEnchantment(CAEcoEnchant(ecoEnchant)) + } + + ecoEnchantOldEnchantments = HashSet(enchantments) + + CustomAnvil.instance.logger.info("Eco Enchant should now work as expected !") + } + + fun handleConfigReload() { + if (isLegacy) { + legacyDependency!!.handleConfigReload() + return + } + + // Should not happen in known case. + if (this.ecoEnchantOldEnchantments == null) return + + val newEnchantments = EcoEnchants.values() + + // Add new enchantments + for (ecoEnchant in newEnchantments) + if (!this.ecoEnchantOldEnchantments!!.contains(ecoEnchant)) + EnchantmentApi.registerEnchantment(CAEcoEnchant(ecoEnchant)) + + // Remove old enchantments that not now currently used + this.ecoEnchantOldEnchantments!!.removeAll(newEnchantments) + for (oldEnchantment in this.ecoEnchantOldEnchantments!!) { + EnchantmentApi.unregisterEnchantment(oldEnchantment.enchantment) + } + + this.ecoEnchantOldEnchantments = HashSet(newEnchantments) + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/EcoItemDependencyUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/EcoItemDependencyUtil.kt new file mode 100644 index 0000000..7e84231 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/EcoItemDependencyUtil.kt @@ -0,0 +1,38 @@ +package xyz.alexcrea.cuanvil.dependency.plugins + +import com.willfp.ecoitems.items.EcoItem +import com.willfp.ecoitems.items.EcoItems +import com.willfp.ecoitems.items.ecoItem +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.inventory.ItemStack + +object EcoItemDependencyUtil { + + fun ecoItemNamespace(item: ItemStack): NamespacedKey? { + val ecoi = item.ecoItem ?: return null + + return ecoi.id + } + + fun ecoItemFromKey(key: NamespacedKey): EcoItem? { + return EcoItems.getByID(key.toString()) + } + + fun ecoItemMaterialFromKey(key: NamespacedKey): Material? { + val ecoi = ecoItemFromKey(key) ?: return null + + return ecoi.itemStack.type + } + + fun newEcoItemstack(key: NamespacedKey): ItemStack? { + val ecoi = ecoItemFromKey(key) ?: return null + + return ecoi.itemStack + } + + fun getItems(): List { + return EcoItems.values().map { item -> item.id } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/EnchantmentSquaredDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/EnchantmentSquaredDependency.kt new file mode 100644 index 0000000..d769986 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/EnchantmentSquaredDependency.kt @@ -0,0 +1,200 @@ +package xyz.alexcrea.cuanvil.dependency.plugins + +import io.delilaheve.CustomAnvil +import me.athlaeos.enchantssquared.enchantments.CustomEnchant +import me.athlaeos.enchantssquared.listeners.AnvilListener +import me.athlaeos.enchantssquared.managers.CustomEnchantManager +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.PrepareAnvilEvent +import org.bukkit.inventory.ItemStack +import org.bukkit.plugin.Plugin +import xyz.alexcrea.cuanvil.api.ConflictBuilder +import xyz.alexcrea.cuanvil.api.EnchantmentApi +import xyz.alexcrea.cuanvil.api.MaterialGroupApi +import xyz.alexcrea.cuanvil.enchant.CAEnchantment +import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry +import xyz.alexcrea.cuanvil.enchant.bulk.EnchantSquaredBulkOperation +import xyz.alexcrea.cuanvil.enchant.wrapped.CAEnchantSquaredEnchantment +import xyz.alexcrea.cuanvil.group.IncludeGroup +import java.util.* + +class EnchantmentSquaredDependency(private val enchantmentSquaredPlugin: Plugin) { + + init { + CustomAnvil.instance.logger.info("Enchantment Squared Detected !") + CustomAnvil.instance.logger.info("Please be aware that Custom Anvil is bypassing Enchantment Squared ") + CustomAnvil.instance.logger.info( + "compatible_with, " + + "disable_anvil, " + + "incompatible_vanilla_enchantments, " + + "incompatible_custom_enchantments and max_level " + + "configuration values.") + } + + fun disableAnvilListener(){ + PrepareAnvilEvent.getHandlerList().unregister(this.enchantmentSquaredPlugin) + + // Find the anvil click event + var toRemove: AnvilListener? = null + for (registered in InventoryClickEvent.getHandlerList().registeredListeners) { + val listener = registered.listener + if(listener is AnvilListener) { + toRemove = listener + break + } + } + + if(toRemove != null) + InventoryClickEvent.getHandlerList().unregister(toRemove) + + } + + fun registerEnchantments(){ + CustomAnvil.instance.logger.info("Preparing Enchantment Squared compatibility...") + + // Register enchantments + for (enchant in CustomEnchantManager.getInstance().allEnchants.values) { + EnchantmentApi.registerEnchantment(CAEnchantSquaredEnchantment(enchant)) + } + + // Register bulk operation + val bulkOpperations = EnchantSquaredBulkOperation() + EnchantmentApi.addBulkGet(bulkOpperations) + EnchantmentApi.addBulkClean(bulkOpperations) + + } + + fun getEnchantmentsSquared(item: ItemStack, enchantments: MutableMap) { + val customEnchants = CustomEnchantManager.getInstance().getItemsEnchantsFromPDC(item) + + customEnchants.forEach{ + (enchantment, level ) -> enchantments[getWrappedEnchant(enchantment)] = level + } + + } + + fun getKeyFromEnchant(enchant: CustomEnchant): NamespacedKey{ + return NamespacedKey.fromString(enchant.type.lowercase(Locale.getDefault()), this.enchantmentSquaredPlugin)!! + } + private fun getWrappedEnchant(enchant: CustomEnchant): CAEnchantment { + return CAEnchantment.getByKey(getKeyFromEnchant(enchant))!! + } + + fun registerPluginConfiguration(){ + CustomAnvil.instance.logger.info("Preparing Enchantment Squared config...") + + // Prepare enchantments + val esEnchantments = ArrayList() + CustomEnchantManager.getInstance().allEnchants.forEach { (_, enchant) -> + esEnchantments.add(getWrappedEnchant(enchant) as CAEnchantSquaredEnchantment) + } + + // Write groups and conflicts + writeMissingGroups() + writeMaterialRestriction(esEnchantments) + writeEnchantmentConflicts(esEnchantments) + + CustomAnvil.instance.logger.info("Enchantment Squared should now work as expected !") + } + + private fun writeMissingGroups(){ + // Write group that do not exist on custom anvil. + val shield = IncludeGroup("shield") + shield.addToPolicy(Material.SHIELD.key) + MaterialGroupApi.addMaterialGroup(shield) + + val elytra = IncludeGroup("elytra") + elytra.addToPolicy(Material.ELYTRA.key) + MaterialGroupApi.addMaterialGroup(elytra) + + val trinkets = IncludeGroup("trinkets") + trinkets.addToPolicy(Material.ROTTEN_FLESH.key) + MaterialGroupApi.addMaterialGroup(trinkets) + } + + private fun writeMaterialRestriction(esEnchantments: List){ + for (enchantment in esEnchantments) { + val conflict = ConflictBuilder("restriction_${enchantment.key.key}", CustomAnvil.instance) + conflict.addEnchantment(enchantment) + + // enchanted book is allowed in any case. + conflict.addExcludedGroup("enchanted_book") + + // Get allowed groups + for (esGroup in enchantment.enchant.compatibleItems) { + val caGroup = esGroupToCAGroup(esGroup) + if(caGroup == null){ + CustomAnvil.instance.logger.info("Could not find equivalent custom anvil group for $esGroup") + continue + } + conflict.addExcludedGroup(caGroup) + } + + conflict.registerIfAbsent() + } + } + + private fun writeEnchantmentConflicts(esEnchantments: List){ + val otherEnchants = ArrayList() + otherEnchants.addAll(CAEnchantmentRegistry.getInstance().values()) + + for (enchantment in esEnchantments) { + otherEnchants.remove(enchantment) + + // find conflicting enchantment. + for (otherEnchant in otherEnchants) { + if(enchantment.enchant.conflictsWithEnchantment(otherEnchant.name)){ + writeConflict(enchantment, otherEnchant) + } + } + } + } + + private fun writeConflict(enchantment1: CAEnchantment, enchantment2: CAEnchantment){ + val conflict = ConflictBuilder("${enchantment1.name}_with_${enchantment2.name}_conflict", CustomAnvil.instance) + + conflict.addEnchantment(enchantment1).addEnchantment(enchantment2) + + conflict.setMaxBeforeConflict(1) + conflict.registerIfAbsent() + } + + /** + * Transform an Enchantment Squared group to a Custom Anvil group + */ + private fun esGroupToCAGroup(esGroup: String): String? { + return when(esGroup){ + "SWORDS" -> "swords" + "BOWS" -> "bow" + "CROSSBOWS" -> "crossbow" + "TRIDENTS" -> "trident" + "HELMETS" -> "helmets" + "CHESTPLATES" -> "chestplate" + "LEGGINGS" -> "leggings" + "BOOTS" -> "boots" + "SHEARS" -> "shears" + "FLINTANDSTEEL" -> "flint_and_steel" + "FISHINGROD" -> "fishing_rod" + "ELYTRA", "ELYTRAS" -> "elytra" + "PICKAXES" -> "pickaxes" // not on this plugin by default + "AXES" -> "axes" + "SHOVELS" -> "shovels" // not on this plugin by default + "HOES" -> "hoes" // not on this plugin by default + "SHIELDS" -> "shield" // not on this plugin by default + "TRINKETS" -> "trinkets" // not the idea way as it will also allow non trinkets rotten flesh to be enchanted. + "ALL" -> "everything" + else -> null + } + } + + fun stripLore(item: ItemStack) { + CustomEnchantManager.getInstance().removeAllEnchants(item) + } + + fun updateLore(item: ItemStack) { + CustomEnchantManager.getInstance().updateLore(item) + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt new file mode 100644 index 0000000..816a4df --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt @@ -0,0 +1,258 @@ +package xyz.alexcrea.cuanvil.dependency.plugins + +import io.delilaheve.CustomAnvil +import org.bukkit.Material +import org.bukkit.event.Listener +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.PrepareAnvilEvent +import org.bukkit.inventory.ItemStack +import org.bukkit.plugin.RegisteredListener +import xyz.alexcrea.cuanvil.api.EnchantmentApi +import xyz.alexcrea.cuanvil.api.event.listener.CATreatAnvilResult2Event +import xyz.alexcrea.cuanvil.enchant.wrapped.CAEEPreV5Enchantment +import xyz.alexcrea.cuanvil.enchant.wrapped.CAEEV5Enchantment +import xyz.alexcrea.cuanvil.enchant.wrapped.CAEEV5_4Enchantment +import xyz.alexcrea.cuanvil.enchant.wrapped.CALegacyEEEnchantment +import java.lang.reflect.Constructor +import java.lang.reflect.Method +import su.nightexpress.excellentenchants.api.EnchantRegistry as V5EnchantRegistry +import su.nightexpress.excellentenchants.enchantment.impl.universal.CurseOfFragilityEnchant as LegacyCurseOfFragilityEnchant +import su.nightexpress.excellentenchants.manager.listener.AnvilListener as V5AnvilListener +import su.nightexpress.excellentenchants.enchantment.listener.AnvilListener as PreV5AnvilListener +import su.nightexpress.excellentenchants.enchantment.listener.EnchantAnvilListener as LegacyAnvilListener +import su.nightexpress.excellentenchants.enchantment.registry.EnchantRegistry as LegacyEnchantRegistry +import su.nightexpress.excellentenchants.registry.EnchantRegistry as PreV5EnchantRegistry + +// I don't like that I need to support older version. if I could just drop older support it would be sooo nice +class ExcellentEnchantsDependency { + + enum class ListenerVersion(val classPath: String) { + V5_4("su.nightexpress.excellentenchants.enchantment.EnchantSettings"), + V5_3("su.nightexpress.excellentenchants.enchantment.EnchantRegistry"), + V5("su.nightexpress.excellentenchants.manager.listener.AnvilListener"), + PRE_V5("su.nightexpress.excellentenchants.enchantment.listener.AnvilListener"), + LEGACY("su.nightexpress.excellentenchants.enchantment.listener.EnchantAnvilListener"), + } + + private val listenerVersion: ListenerVersion? + private val isModernCurseOfFragility: Boolean + + init { + CustomAnvil.instance.logger.info("Excellent Enchants Detected !") + + var listenerVersion: ListenerVersion? = null + for (value in ListenerVersion.entries) { + try { + Class.forName(value.classPath) + + listenerVersion = value + break + } catch (ignored: ClassNotFoundException) { + } + } + + if (listenerVersion == null) { + CustomAnvil.instance.logger.severe("Found issue with listener of Excellent Enchants. compatiblity is broken. please contact CustomAnvil devs") + } else{ + CustomAnvil.log("Support version: " + listenerVersion.name) + } + + var isModernCurseOfFragility = true + try { + Class.forName("su.nightexpress.excellentenchants.enchantment.universal.CurseOfFragilityEnchant") + } catch (ignored: ClassNotFoundException) { + isModernCurseOfFragility = false + } + + this.listenerVersion = listenerVersion + this.isModernCurseOfFragility = isModernCurseOfFragility + } + + fun registerEnchantments() { + CustomAnvil.instance.logger.info("Preparing Excellent Enchants compatibility...") + + // As excellent enchants is loaded before custom anvil and register enchantment to registry, we need to unregister old "vanilla" enchant. + when (listenerVersion) { + ListenerVersion.V5_4 -> { + for (enchantment in ExcellentEnchant5_3Registry.getRegistered()) { + EnchantmentApi.unregisterEnchantment(enchantment.bukkitEnchantment.key) + EnchantmentApi.registerEnchantment(CAEEV5_4Enchantment(enchantment)) + } + } + + ListenerVersion.V5, ListenerVersion.V5_3 -> { + for (enchantment in V5EnchantRegistry.getRegistered()) { + EnchantmentApi.unregisterEnchantment(enchantment.bukkitEnchantment.key) + EnchantmentApi.registerEnchantment(CAEEV5Enchantment(enchantment)) + } + } + + ListenerVersion.PRE_V5 -> { + for (enchantment in PreV5EnchantRegistry.getRegistered()) { + EnchantmentApi.unregisterEnchantment(enchantment.bukkitEnchantment.key) + EnchantmentApi.registerEnchantment(CAEEPreV5Enchantment(enchantment)) + } + } + + ListenerVersion.LEGACY -> { + for (enchantment in LegacyEnchantRegistry.getRegistered()) { + EnchantmentApi.unregisterEnchantment(enchantment.enchantment.key) + EnchantmentApi.registerEnchantment(CALegacyEEEnchantment(enchantment)) + } + } + + null -> return + + } + + CustomAnvil.instance.logger.info("Excellent Enchants should now work as expected !") + } + + private var legacyFragilityCurse: LegacyCurseOfFragilityEnchant? = null + + private var v5AnvilListener: V5AnvilListener? = null + private var preV5AnvilListener: PreV5AnvilListener? = null + private var legacyAnvilListener: LegacyAnvilListener? = null + private lateinit var usedAnvilListener: Listener + + private lateinit var handleRechargeMethod: Method + private lateinit var handleCombineMethod: Method + + private val prepareAnvilConstructor = PrepareAnvilEvent::class.java.constructors.first() as Constructor + + fun redirectListeners() { + val toUnregister = ArrayList() + // get required PrepareAnvilEvent listener + for (registeredListener in PrepareAnvilEvent.getHandlerList().registeredListeners) { + val listener = registeredListener.listener + + if (!isModernCurseOfFragility) { + if (listener is LegacyCurseOfFragilityEnchant) { + this.legacyFragilityCurse = listener + toUnregister.add(registeredListener) + } + } + + when (listenerVersion) { + ListenerVersion.V5, + ListenerVersion.V5_3, + ListenerVersion.V5_4, + -> { + if (listener is V5AnvilListener) { + this.v5AnvilListener = listener + toUnregister.add(registeredListener) + } + } + + ListenerVersion.PRE_V5 -> { + if (listener is PreV5AnvilListener) { + this.preV5AnvilListener = listener + toUnregister.add(registeredListener) + } + } + + ListenerVersion.LEGACY -> { + if (listener is LegacyAnvilListener) { + this.legacyAnvilListener = listener + toUnregister.add(registeredListener) + } + } + + null -> { + } + } + + } + + for (listener in toUnregister) { + PrepareAnvilEvent.getHandlerList().unregister(listener) + } + + when (listenerVersion) { + ListenerVersion.V5_3, + ListenerVersion.V5, + ListenerVersion.V5_4, + -> this.usedAnvilListener = v5AnvilListener!! + ListenerVersion.PRE_V5 -> this.usedAnvilListener = preV5AnvilListener!! + ListenerVersion.LEGACY -> this.usedAnvilListener = legacyAnvilListener!! + null -> {} + } + + // Unregister inventory click event + InventoryClickEvent.getHandlerList().unregister(this.usedAnvilListener) + + findAnvilFunctions() + } + + private fun findAnvilFunctions() { + this.handleRechargeMethod = this.usedAnvilListener.javaClass.getDeclaredMethod( + "handleRecharge", + PrepareAnvilEvent::class.java, ItemStack::class.java, ItemStack::class.java + ) + this.handleRechargeMethod.setAccessible(true) + + try { + this.handleCombineMethod = this.usedAnvilListener.javaClass.getDeclaredMethod( + "anvilCombine", + PrepareAnvilEvent::class.java, ItemStack::class.java, ItemStack::class.java, ItemStack::class.java + ) + this.handleCombineMethod.setAccessible(true) + } catch (_: NoSuchMethodException) { + this.handleCombineMethod = this.usedAnvilListener.javaClass.getDeclaredMethod( + "handleCombine", + PrepareAnvilEvent::class.java, ItemStack::class.java, ItemStack::class.java, ItemStack::class.java + ) + this.handleCombineMethod.setAccessible(true) + } + + } + + fun testPrepareAnvil(event: PrepareAnvilEvent): Boolean { + if (event.result != null) { + if (!isModernCurseOfFragility) { + this.legacyFragilityCurse?.onItemAnvil(event) + } + if (event.result == null) return true + } + + val first: ItemStack = treatInput(event.inventory.getItem(0)) + val second: ItemStack = treatInput(event.inventory.getItem(1)) + + return handleRechargeMethod.invoke(this.usedAnvilListener, event, first, second) as Boolean + } + + fun treatAnvilResult(event: CATreatAnvilResult2Event) { + val result = event.result ?: return + + val first: ItemStack = treatInput(event.leftItem) + val second: ItemStack = treatInput(event.rightItem) + val fakeEvent = prepareAnvilConstructor.newInstance(event.view, result) + + handleCombineMethod.invoke(this.usedAnvilListener, fakeEvent, first, second, result) + + event.result = fakeEvent.result + } + + fun testAnvilResult(event: InventoryClickEvent): Any { + if (event.inventory.getItem(2) != null) { + when (listenerVersion) { + ListenerVersion.V5, + ListenerVersion.V5_3, + ListenerVersion.V5_4, + -> v5AnvilListener!!.onClickAnvil(event) + ListenerVersion.PRE_V5 -> preV5AnvilListener!!.onClickAnvil(event) + ListenerVersion.LEGACY -> legacyAnvilListener!!.onClickAnvil(event) + null -> {} + } + return event.inventory.getItem(2) == null + } + + return false + } + + private fun treatInput(item: ItemStack?): ItemStack { + if (item == null) return ItemStack(Material.AIR) + return item + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/GenericPluginDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/GenericPluginDependency.kt new file mode 100644 index 0000000..62dae9b --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/GenericPluginDependency.kt @@ -0,0 +1,81 @@ +package xyz.alexcrea.cuanvil.dependency.plugins + +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.PrepareAnvilEvent +import org.bukkit.plugin.Plugin +import org.bukkit.plugin.RegisteredListener + +open class GenericPluginDependency(protected open val plugin: Plugin, private val testPrepare: Boolean = true) { + + private val preAnvil = ArrayList() + private val postAnvil = ArrayList() + + open fun redirectListeners() { + fillPreAnvil(preAnvil) + fillPostAnvil(postAnvil, preAnvil) + + // get required PrepareAnvilEvent listener + for (listener in preAnvil) { + PrepareAnvilEvent.getHandlerList().unregister(listener) + } + + for (listener in postAnvil) { + InventoryClickEvent.getHandlerList().unregister(listener) + } + } + + open fun fillPreAnvil(preAnvil: ArrayList){ + // get PreAnvil and PostAnvil listeners + for (registeredListener in PrepareAnvilEvent.getHandlerList().registeredListeners) { + + if (registeredListener.plugin != plugin) continue + preAnvil.add(registeredListener) + } + } + + protected open fun fillPostAnvil( + postAnvil: ArrayList, + preAnvil: ArrayList) { + + } + + open fun testPrepareAnvil(event: PrepareAnvilEvent): Boolean { + if(!testPrepare) return false + + val previousResult = event.result + event.result = null + + for (registeredListener in preAnvil) { + // We do not want error from another plugin to be our fault + try { + registeredListener.callEvent(event) + } catch (e: Exception) { + e.printStackTrace() + } + + if (event.result != null) return true + } + + event.result = previousResult; + return false + } + + open fun testAnvilResult(event: InventoryClickEvent): Boolean { + if(!testPrepare) return false + + for (registeredListener in postAnvil) { + // We do not want error from another plugin to be our fault + try { + registeredListener.callEvent(event) + } catch (e: Exception) { + e.printStackTrace() + } + + if (event.inventory.getItem(2) == null) return true + } + + return false + } + + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt new file mode 100644 index 0000000..6f30497 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt @@ -0,0 +1,85 @@ +package xyz.alexcrea.cuanvil.dependency.plugins + +import io.delilaheve.CustomAnvil +import org.bukkit.entity.Player +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.PrepareAnvilEvent +import org.bukkit.inventory.AnvilInventory +import org.bukkit.plugin.RegisteredListener +import valorless.havenbags.HavenBags +import valorless.havenbags.features.BagSkin +import valorless.havenbags.features.BagUpgrade +import xyz.alexcrea.cuanvil.anvil.AnvilCost +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil + +class HavenBagsDependency { + + init { + CustomAnvil.instance.logger.info("Heaven Bags Detected !") + } + + private lateinit var bagUpgrade: BagUpgrade + private lateinit var bagSkin: BagSkin + + fun redirectListeners() { + val toUnregister = ArrayList() + // get required PrepareAnvilEvent listener + for (registeredListener in PrepareAnvilEvent.getHandlerList().registeredListeners) { + val listener = registeredListener.listener + + if (listener is BagUpgrade) { + bagUpgrade = listener + toUnregister.add(registeredListener) + } + + if (listener is BagSkin) { + bagSkin = listener + toUnregister.add(registeredListener) + } + } + + for (listener in toUnregister) { + PrepareAnvilEvent.getHandlerList().unregister(listener) + InventoryClickEvent.getHandlerList().unregister(listener) + } + + } + + fun testPrepareAnvil(event: PrepareAnvilEvent, player: Player): Boolean { + val previousResult = event.result + event.result = null + + // Test if event change the result + bagSkin.onPrepareAnvil(event) + if (event.result != null) { + CustomAnvil.log("Detected pre anvil heaven bag anvil skin.") + AnvilXpUtil.setAnvilInvCost(event.inventory, event.view, player, AnvilCost(event.inventory.repairCost)) + return true + } + + bagUpgrade.onPrepareAnvil(event) + if (event.result != null) { + CustomAnvil.log("Detected pre anvil heaven bag anvil upgrade.") + AnvilXpUtil.setAnvilInvCost(event.inventory, event.view, player, AnvilCost(event.inventory.repairCost)) + return true + } + + event.result = previousResult + return false + } + + fun testAnvilResult(event: InventoryClickEvent, inventory: AnvilInventory): Boolean { + val result = inventory.getItem(PrepareAnvilListener.ANVIL_OUTPUT_SLOT)?.clone() + + if (HavenBags.IsBag(result)) { + CustomAnvil.log("Detected anvil click haven bag bypass.") + bagUpgrade.onInventoryClick(event) + bagSkin.onInventoryClick(event) + return true + } + + return false + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ItemsAdderDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ItemsAdderDependency.kt new file mode 100644 index 0000000..7f977e1 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ItemsAdderDependency.kt @@ -0,0 +1,42 @@ +package xyz.alexcrea.cuanvil.dependency.plugins + +import dev.lone.itemsadder.api.CustomStack +import dev.lone.itemsadder.api.ItemsAdder +import org.bukkit.NamespacedKey +import org.bukkit.inventory.ItemStack +import org.bukkit.plugin.Plugin + +class ItemsAdderDependency(plugin: Plugin) : GenericPluginDependency(plugin) { + var isLoaded: Boolean = false + get() { + if (field) return true + + // We can't be sure the event is registered before being triggered so we need to use this function + field = ItemsAdder.areItemsLoaded() + return field + } + + fun tryClone(item: ItemStack): ItemStack? { + if(!isLoaded) return null + val customItem = CustomStack.byItemStack(item) ?: return null + + return CustomStack.getInstance(customItem.namespacedID)?.itemStack + } + + fun fromKey(key: NamespacedKey): ItemStack? { + if(!isLoaded) return null + return CustomStack.getInstance(key.toString())?.itemStack + } + + fun getKey(item: ItemStack) : NamespacedKey? { + if(!isLoaded) return null + val customItem = CustomStack.byItemStack(item) ?: return null + + return NamespacedKey.fromString(customItem.namespacedID) + } + + fun idsCount(): Set { + return CustomStack.getNamespacedIdsInRegistry() + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/LegacyEcoEnchantDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/LegacyEcoEnchantDependency.kt new file mode 100644 index 0000000..60f00b8 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/LegacyEcoEnchantDependency.kt @@ -0,0 +1,46 @@ +package xyz.alexcrea.cuanvil.dependency.plugins + +import com.willfp.ecoenchants.enchantments.EcoEnchant +import com.willfp.ecoenchants.enchantments.EcoEnchants +import org.bukkit.enchantments.Enchantment +import xyz.alexcrea.cuanvil.api.EnchantmentApi +import xyz.alexcrea.cuanvil.enchant.wrapped.CALegacyEcoEnchant + +class LegacyEcoEnchantDependency { + + + private var ecoEnchantOldEnchantments: MutableSet? = null + fun registerEnchantments() { + val enchantments = EcoEnchants.values() + for (ecoEnchant in enchantments) { + ecoEnchant as Enchantment + + EnchantmentApi.unregisterEnchantment(ecoEnchant) // As eco enchants is loaded before custom anvil and register enchantment to registry, we need to unregister old "vanilla" enchant. + EnchantmentApi.registerEnchantment(CALegacyEcoEnchant(ecoEnchant, ecoEnchant)) + } + + ecoEnchantOldEnchantments = HashSet(enchantments) + } + + fun handleConfigReload() { + // Should not happen in known case. + if (this.ecoEnchantOldEnchantments == null) return + + val newEnchantments = EcoEnchants.values() + + // Add new enchantments + for (ecoEnchant in newEnchantments) + if (!this.ecoEnchantOldEnchantments!!.contains(ecoEnchant)) + EnchantmentApi.registerEnchantment(CALegacyEcoEnchant(ecoEnchant, ecoEnchant as Enchantment)) + + + // Remove old enchantments that not now currently used + this.ecoEnchantOldEnchantments!!.removeAll(newEnchantments) + for (oldEnchantment in this.ecoEnchantOldEnchantments!!) { + EnchantmentApi.unregisterEnchantment(oldEnchantment as Enchantment) + } + + this.ecoEnchantOldEnchantments = HashSet(newEnchantments) + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/SuperEnchantDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/SuperEnchantDependency.kt new file mode 100644 index 0000000..11622f9 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/SuperEnchantDependency.kt @@ -0,0 +1,91 @@ +package xyz.alexcrea.cuanvil.dependency.plugins + +import com.maddoxh.superEnchants.SuperEnchants +import com.maddoxh.superEnchants.enchants.EnchantManager +import com.maddoxh.superEnchants.listeners.AnvilListener +import io.delilaheve.CustomAnvil +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.plugin.RegisteredListener +import xyz.alexcrea.cuanvil.api.EnchantmentApi +import xyz.alexcrea.cuanvil.enchant.bulk.SuperEnchantBulkOperation +import xyz.alexcrea.cuanvil.enchant.wrapped.CASuperEnchantEnchantment +import java.util.logging.Level + +class SuperEnchantDependency(override val plugin: SuperEnchants): GenericPluginDependency(plugin, false) { + + lateinit var enchManager: EnchantManager + val enchantments = ArrayList() + + fun registerEnchantments(): Boolean{ + CustomAnvil.instance.logger.info("Preparing Super Enchant compatibility...") + + val field = SuperEnchants::class.java.getDeclaredField("enchantManager") + if(field == null) { + CustomAnvil.instance.logger.log(Level.SEVERE, "Failed to initialize Super Enchant compatibility") + return false + } + field.setAccessible(true) + + val bulkOpperations = SuperEnchantBulkOperation(plugin) + EnchantmentApi.addBulkGet(bulkOpperations) + EnchantmentApi.addBulkClean(bulkOpperations) + + enchManager = field.get(plugin) as EnchantManager + overrideReloadCommand() + + reload() + return true + } + + fun reload() { + for (enchantment in enchantments) { + EnchantmentApi.unregisterEnchantment(enchantment) + } + enchantments.clear() + + // Register enchantments + for (enchant in enchManager.getAll()) { + val enchantment = CASuperEnchantEnchantment(enchant, plugin, enchManager) + enchantments.add(enchantment) + + EnchantmentApi.registerEnchantment(enchantment) + } + } + + private fun overrideReloadCommand() { + val reload = CustomAnvil.instance.getCommand("sereload") + + reload?.setExecutor(ReloadInterceptor(reload.executor)) + } + + inner class ReloadInterceptor(val other: CommandExecutor): CommandExecutor { + + override fun onCommand( + sender: CommandSender, + command: Command, + label: String, + args: Array + ): Boolean { + val result = other.onCommand(sender, command, label, args) + + CustomAnvil.log("Detected SuperEnchant reload") + reload() + + return result + } + + } + + override fun fillPostAnvil(postAnvil: ArrayList, preAnvil: ArrayList) { + + for (registeredListener in InventoryClickEvent.getHandlerList().registeredListeners) { + + if (registeredListener.listener.javaClass != AnvilListener::class.java) continue + postAnvil.add(registeredListener) + } + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ToolStatsDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ToolStatsDependency.kt new file mode 100644 index 0000000..255f737 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ToolStatsDependency.kt @@ -0,0 +1,39 @@ +package xyz.alexcrea.cuanvil.dependency.plugins + +import lol.hyper.toolstats.ToolStats +import lol.hyper.toolstats.tools.ItemChecker +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.inventory.ItemStack +import org.bukkit.plugin.Plugin +import org.bukkit.plugin.RegisteredListener +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener +import java.lang.reflect.Method + +class ToolStatsDependency(plugin: Plugin) : GenericPluginDependency(plugin) { + + // Sadly, getTokens function is private, so I need to do some reflectino + private val getTokenMethod: Method = + ItemChecker::class.java.getDeclaredMethod("getTokens", ItemStack::class.java); + + init { + getTokenMethod.trySetAccessible() + } + + private fun ItemChecker.getTokenSafe(item: ItemStack?): Array { + if (item == null) return arrayOf() + return getTokenMethod.invoke(this, item) as Array + } + + override fun testAnvilResult(event: InventoryClickEvent): Boolean { + // Check if token changes from left with result + val left = event.inventory.getItem(PrepareAnvilListener.ANVIL_INPUT_LEFT) + val result = event.inventory.getItem(PrepareAnvilListener.ANVIL_OUTPUT_SLOT) + + val itemChecker = (plugin as ToolStats).itemChecker + + val leftTokens = itemChecker.getTokenSafe(left) + val resultToken = itemChecker.getTokenSafe(result) + + return !leftTokens.contentDeepEquals(resultToken); + } +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/scheduler/BukkitScheduler.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/scheduler/BukkitScheduler.kt new file mode 100644 index 0000000..8c04162 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/scheduler/BukkitScheduler.kt @@ -0,0 +1,17 @@ +package xyz.alexcrea.cuanvil.dependency.scheduler + +import org.bukkit.Bukkit +import org.bukkit.entity.Entity +import org.bukkit.plugin.Plugin + +class BukkitScheduler : TaskScheduler { + + override fun scheduleGlobally(plugin: Plugin, task: Runnable, time: Long): Any { + return Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, task, time) + } + + + override fun scheduleOnEntity(plugin: Plugin, entity: Entity, task: Runnable, time: Long): Any { + return Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, task, time) + } +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/AbstractMaterialGroup.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/AbstractMaterialGroup.kt new file mode 100644 index 0000000..d0d2bda --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/AbstractMaterialGroup.kt @@ -0,0 +1,123 @@ +package xyz.alexcrea.cuanvil.group + +import org.bukkit.Material +import org.bukkit.NamespacedKey +import xyz.alexcrea.cuanvil.util.MaterialUtil + +abstract class AbstractMaterialGroup(private val name: String) { + protected val includedMaterial by lazy { createDefaultSet() } + + /** + * Get the group default set + */ + protected abstract fun createDefaultSet(): MutableSet + + /** + * Get if a material is allowed following the group policy + */ + open fun contain(mat: NamespacedKey): Boolean { + return mat in getMaterials() + } + + /** + * Get if a group is referenced by this: + */ + abstract fun isReferencing(other: AbstractMaterialGroup): Boolean + + /** + * Push a material to this group to follow this group policy + * @return this instance. + */ + abstract fun addToPolicy(type: NamespacedKey): AbstractMaterialGroup + + /** + * Push a list of material to this group to follow this group policy + * @return this instance. + */ + fun addAll(vararg materials: NamespacedKey): AbstractMaterialGroup { + for (material in materials) { + addToPolicy(material) + } + return this + } + + /** + * Push a group to this group to follow this group policy + * @return this instance. + */ + abstract fun addToPolicy(other: AbstractMaterialGroup): AbstractMaterialGroup + + /** + * Push a list of group to this group to follow this group policy + * @return this instance. + */ + fun addAll(vararg otherList: AbstractMaterialGroup): AbstractMaterialGroup { + for (group in otherList) { + addToPolicy(group) + } + return this + } + + /** + * Get the group contained material as a set + */ + abstract fun getMaterials(): Set + + /** + * Get the group non-inherited material as a set + */ + open fun getNonGroupInheritedMaterials(): Set { + return includedMaterial + } + + /** + * Get the group non-inherited material as a set + */ + open fun setNonGroupInheritedMaterials(materials: Set) { + this.includedMaterial.clear() + this.includedMaterial.addAll(materials) + } + + /** + * Get the group name in case something is wrong + */ + open fun getName(): String { + return name + } + + override fun toString(): String { + return name + } + + /** + * Update the contained groups of this group + */ + abstract fun setGroups(groups: MutableSet) + + /** + * Get the contained group of this material group + */ + abstract fun getGroups(): MutableSet + + open fun getRepresentativeMaterial(): Material { + // Test inner material + val matIterator = includedMaterial.iterator() + while (matIterator.hasNext()) { + val key = matIterator.next() + val material = MaterialUtil.getMatFromKey(key) + if (material == null || material.isAir) continue + return material + } + // Test included group representative material + val groupIterator = getGroups().iterator() + while (groupIterator.hasNext()) { + val groupMat = groupIterator.next().getRepresentativeMaterial() + if (groupMat.isAir) continue + return groupMat + } + return Material.PAPER + } + + abstract fun updateMaterials() + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt new file mode 100644 index 0000000..59841ac --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt @@ -0,0 +1,144 @@ +package xyz.alexcrea.cuanvil.group + +import io.delilaheve.CustomAnvil +import org.bukkit.Material +import org.bukkit.NamespacedKey +import xyz.alexcrea.cuanvil.enchant.CAEnchantment + +class EnchantConflictGroup( + val name: String, + private val cantConflict: AbstractMaterialGroup, + var minBeforeBlock: Int, +) { + + private val enchantments = HashSet() + private val conflictsAfterLevel = HashMap() + private val conflictsBeforeLevel = HashMap() + + fun addEnchantment(enchant: CAEnchantment) { + enchantments.add(enchant) + } + fun addEnchantments(enchants: List) { + enchantments.addAll(enchants) + } + + private fun canBypassByBeforeLevel(enchants: Map): Boolean { + // Either there no "conflict after" + if(conflictsAfterLevel.isEmpty()) return false + + // Or we check if any conflict after enchantment is true + for (entry in conflictsAfterLevel) { + val current = enchants.getOrDefault(entry.key, 0) + if(current > entry.value) + return false + } + + return true + } + + private fun canBypassByAfterLevel(enchants: Map): Boolean { + // Either there no "conflict after" + if(conflictsBeforeLevel.isEmpty()) return false + + // Or we check if any conflict after enchantment is true + for (entry in conflictsBeforeLevel) { + val current = enchants.getOrDefault(entry.key, 0) + if(current < entry.value) + return false + } + + return true + } + + private fun canBypassConflictByLevel(enchants: Map): Boolean { + return canBypassByBeforeLevel(enchants) || canBypassByAfterLevel(enchants) + } + + fun allowed(enchants: Map, mat: NamespacedKey): Boolean { + if (enchantments.size < minBeforeBlock) { + CustomAnvil.verboseLog("Conflicting bc of to many enchantments") + return true + } + + if (cantConflict.contain(mat)) + return true + + // If empty we skip. else we + if(canBypassConflictByLevel(enchants)) + return true + + // Count the amount of enchantment that are in the list + var enchantAmount = 0 + for (entry in enchants) { + val enchantment = entry.key + + if (enchantment !in enchantments) continue + CustomAnvil.verboseLog("Enchant ${enchantment.key} is in: ${enchantAmount + 1}/$minBeforeBlock ") + if (++enchantAmount > minBeforeBlock) { + CustomAnvil.verboseLog("it is not allowed bc of to many enchantment in conflict") + return false + } + + } + return true + } + + fun getCantConflictGroup(): AbstractMaterialGroup { + return this.cantConflict + } + + fun getEnchants(): HashSet { + return enchantments + } + + fun setEnchants(enchants: Set) { + enchantments.clear() + enchantments.addAll(enchants) + } + + fun getConflictAfters(): HashMap { + return conflictsAfterLevel + } + + fun putConflictAfterLevel(enchantment: CAEnchantment, level: Int): Boolean { + return null != ( + if(level < 0) conflictsAfterLevel.remove(enchantment) + else conflictsAfterLevel.put(enchantment, level)) + } + + fun setConflictsAfterLevel(conflictAfterLevel: HashMap) { + this.conflictsAfterLevel.clear() + this.conflictsAfterLevel.putAll(conflictAfterLevel) + } + + fun getConflictsBefore(): HashMap { + return conflictsBeforeLevel + } + + fun putConflictsBeforeLevel(enchantment: CAEnchantment, level: Int): Boolean { + return null != ( + if(level < 0) conflictsBeforeLevel.remove(enchantment) + else conflictsBeforeLevel.put(enchantment, level)) + } + + fun setConflictsBeforeLevel(conflictBeforeLevel: HashMap) { + this.conflictsBeforeLevel.clear() + this.conflictsBeforeLevel.putAll(conflictBeforeLevel) + } + + fun getRepresentativeMaterial(): Material { + val groups = getCantConflictGroup().getGroups() + val groupIterator = groups.iterator() + while (groupIterator.hasNext()) { + val mat = groupIterator.next().getRepresentativeMaterial() + if (mat != Material.ENCHANTED_BOOK) return mat + + } + return Material.ENCHANTED_BOOK + } + + override fun toString(): String { + return name + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt new file mode 100644 index 0000000..38d5476 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt @@ -0,0 +1,312 @@ +package xyz.alexcrea.cuanvil.group + +import io.delilaheve.CustomAnvil +import org.bukkit.NamespacedKey +import org.bukkit.configuration.ConfigurationSection +import org.bukkit.enchantments.Enchantment +import org.bukkit.inventory.ItemStack +import xyz.alexcrea.cuanvil.enchant.AdditionalTestEnchantment +import xyz.alexcrea.cuanvil.enchant.CAEnchantment +import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry +import xyz.alexcrea.cuanvil.util.MaterialUtil.customType +import java.util.* +import kotlin.collections.set + +class EnchantConflictManager { + + companion object { + // Path for the enchantments list + const val ENCH_LIST_PATH = "enchantments" + + // Path for list of enchantments conflicting before level + //TODO add test and gui + const val CONFLICT_AFTER_LEVEL_LIST_PATH = "conflict_after_level" + + // Path for list of enchantments conflicting before level + //TODO add test and gui + const val CONFLICT_BEFORE_LEVEL_LIST_PATH = "conflict_before_level" + + // Path for group list related to the conflict + const val CONFLICT_GROUP_PATH = "notAffectedGroups" + + // Path for the maximum number of enchantment before validating the conflict + const val ENCH_MAX_PATH = "maxEnchantmentBeforeConflict" + + // Path for a flag: if the enchantment will be used in the last supported version + // TODO maybe replace this system by a list of "future" enchantment. + private const val FUTURE_USE_PATH = "useInFuture" + + // Default name for a joining group + const val DEFAULT_GROUP_NAME = "joinedGroup" + + // 1.20.5 compatibility TODO better update system + private val SWEEPING_EDGE_ENCHANT = Collections.singletonList( + CAEnchantment.getByKey(NamespacedKey.minecraft("sweeping_edge")) + ?: CAEnchantment.getByKey(Enchantment.SWEEPING_EDGE.key) + ) + + } + + lateinit var conflictList: ArrayList + + + private fun warnBadKey(key: String) { + CustomAnvil.instance.logger.warning("Invalid key $key for conflict: is not a conflict") + } + + // Read and prepare all conflict + fun prepareConflicts(config: ConfigurationSection, itemManager: ItemGroupManager) { + conflictList = ArrayList() + + // Clear conflict if exist + for (enchant in CAEnchantmentRegistry.getInstance().values()) { + enchant.clearConflict() + } + + val keys = config.getKeys(false) + for (key in keys) { + val section = config.getConfigurationSection(key) + if(section == null) { + warnBadKey(key) + continue + } + val conflict = createConflict(section, itemManager, key) + + addConflict(conflict) + } + + } + + fun addConflict(conflict: EnchantConflictGroup) { + addConflictToEnchantments(conflict) + conflictList.add(conflict) + } + + fun removeConflict(conflict: EnchantConflictGroup) { + removeConflictFromEnchantments(conflict) + conflictList.remove(conflict) + } + + // Add the conflict to enchantments + private fun addConflictToEnchantments(conflict: EnchantConflictGroup) { + conflict.getEnchants().forEach { enchant -> + enchant.addConflict(conflict) + } + } + + // Remove the conflict from enchantments + private fun removeConflictFromEnchantments(conflict: EnchantConflictGroup) { + conflict.getEnchants().forEach { enchant -> + enchant.removeConflict(conflict) + } + } + + // create and read a conflict from a yaml section + private fun createConflict( + section: ConfigurationSection, + itemManager: ItemGroupManager, + conflictName: String + ): EnchantConflictGroup { + // Is it planed for the future + val futureUse = section.getBoolean(FUTURE_USE_PATH, false) + // Create conflict + val conflict = createConflictObject(section, itemManager, conflictName) + // Read and add enchantment to conflict + val enchantList = section.getStringList(ENCH_LIST_PATH) + for (enchantName in enchantList) { + val enchants = getEnchantByIdentifier(enchantName) + if (enchants.isEmpty()) { + if (!futureUse) { //TODO future use will be deprecated once the new update system is finished + CustomAnvil.instance.logger.warning("Enchantment $enchantName do not exist but was asked for conflict $conflictName") + } + continue + } + conflict.addEnchantments(enchants) + } + if (conflict.getEnchants().isEmpty()) { + if (!futureUse) { //TODO future use will be deprecated once the new update system is finished + CustomAnvil.instance.logger.warning("Conflict $conflictName do not have valid enchantment, it will not do anything") + } + } + + val conflictsAfterLevel = section.getConfigurationSection(CONFLICT_AFTER_LEVEL_LIST_PATH) + val conflictsAfterMap = conflict.getConflictAfters() + fetchConditionalRestriction(conflictsAfterMap, conflictsAfterLevel, conflictName) + + val conflictsBeforeLevel = section.getConfigurationSection(CONFLICT_BEFORE_LEVEL_LIST_PATH) + val conflictsBeforeMap = conflict.getConflictsBefore() + fetchConditionalRestriction(conflictsBeforeMap, conflictsBeforeLevel, conflictName) + + return conflict + } + + private fun fetchConditionalRestriction(restrictions: MutableMap, section: ConfigurationSection?, conflictName: String) { + if(section == null) return + for (enchantName in section.getKeys(false)) { + val enchants = getEnchantByIdentifier(enchantName) + if (enchants.isEmpty()) { + CustomAnvil.instance.logger.warning("Enchantment $enchantName do not exist but was asked for conditional restriction for conflict $conflictName") + continue + } + + val value = section.getInt(enchantName, -1) + if(value < 0) continue + + for (enchant in enchants) { + val previous = restrictions.getOrDefault(enchant, value) + restrictions[enchant] = value.coerceAtMost(previous) + } + } + } + + private fun getEnchantByIdentifier(enchantName: String): List { + val key = NamespacedKey.fromString(enchantName) + if (key != null) { + val enchantment = CAEnchantment.getByKey(key) + if (enchantment != null) return Collections.singletonList(enchantment) + + } + + // Temporary solution for 1.20.5 + when (enchantName) { + "minecraft:sweeping", "sweeping", + "minecraft:sweeping_edge", "sweeping_edge" -> { + return SWEEPING_EDGE_ENCHANT + } + } + + return CAEnchantment.getListByName(enchantName) + } + + + private fun createConflictObject( + section: ConfigurationSection, + itemManager: ItemGroupManager, + conflictName: String + ): EnchantConflictGroup { + // Get the maximum number of enchantment before validating the conflict + var minBeforeBlock = section.getInt(ENCH_MAX_PATH, 0) + if (minBeforeBlock < 0) { + minBeforeBlock = 0 + CustomAnvil.instance.logger.warning("Conflict $conflictName have an invalid value of $ENCH_MAX_PATH") + CustomAnvil.instance.logger.warning("It should be more or equal to 0. default to 0") + } + // Find or create the selected group for the conflict + val groupList = section.getStringList(CONFLICT_GROUP_PATH) + val finalGroup = IncludeGroup(DEFAULT_GROUP_NAME) + for (groupName in groupList) { + finalGroup.addToPolicy(findGroup(groupName, itemManager, conflictName)) + } + + // Return conflict + return EnchantConflictGroup(conflictName, finalGroup, minBeforeBlock) + } + + private fun findGroup( + groupName: String, + itemManager: ItemGroupManager, + conflictName: String + ): AbstractMaterialGroup { + val group = itemManager.get(groupName) + if (group == null) { + CustomAnvil.instance.logger.warning("Material group $groupName do not exist but is ask by conflict $conflictName") + return IncludeGroup("error_placeholder") + } + + return group + } + + fun isConflicting( + appliedEnchants: Map, + item: ItemStack, + newEnchant: CAEnchantment + ): ConflictType { + val type = item.customType + CustomAnvil.verboseLog("Testing conflict for ${newEnchant.key} on ${type}") + val conflictList = newEnchant.conflicts + + var result = ConflictType.NO_CONFLICT + for (conflict in conflictList) { + val isBigConflict = conflict.getEnchants().size > 1 + if (result == ConflictType.ITEM_CONFLICT && !isBigConflict) { + CustomAnvil.verboseLog("skipping small conflict ${conflict.name}") + continue + } + + val allowed = conflict.allowed(appliedEnchants, type) + CustomAnvil.verboseLog("Was against $conflict and conflicting: ${!allowed} ") + if (!allowed) { + if (conflict.getEnchants().size <= 1) { + result = ConflictType.ITEM_CONFLICT + CustomAnvil.verboseLog("Small conflict (${conflict.name}), continuing") + } else { + CustomAnvil.verboseLog("Big conflict (${conflict.name}), stopping") + return ConflictType.ENCHANTMENT_CONFLICT + } + } + } + + val immutableEnchants = Collections.unmodifiableMap(appliedEnchants) + for (appliedEnchant in appliedEnchants.keys) { + if (appliedEnchant is AdditionalTestEnchantment) { + val doConflict = appliedEnchant.isEnchantConflict(immutableEnchants, type) + if (doConflict) { + CustomAnvil.verboseLog("Big conflict by additional test, stopping") + return ConflictType.ENCHANTMENT_CONFLICT + } + + } + } + + if ((result != ConflictType.ITEM_CONFLICT) && (newEnchant is AdditionalTestEnchantment)) { + val partialItem = createPartialResult(item, immutableEnchants) + + if (newEnchant.isItemConflict(immutableEnchants, type, partialItem)) { + return ConflictType.ITEM_CONFLICT + } + + } + + return result + } + + private fun createPartialResult(item: ItemStack, enchantments: Map): ItemStack { + val newItem = item.clone() + + CAEnchantment.clearEnchants(newItem) + enchantments.forEach { enchantment -> + enchantment.key.addEnchantmentUnsafe(newItem, enchantment.value) + } + + return newItem + } + +} + +/** + * Provide information about the current conflict. + */ +enum class ConflictType(private val importance: Int) { + /** + * Allowed to proceed the anvil process. + */ + NO_CONFLICT(0), + + /** + * Inform that the anvil process should not change the current applied enchantment. + */ + ITEM_CONFLICT(1), + + /** + * Inform that the anvil process should not change the current applied enchantment. + * Also add sacrificeIllegalCost for every enchantment marked as big conflict. + */ + ENCHANTMENT_CONFLICT(2); + + fun getWorstConflict(otherConflict: ConflictType): ConflictType { + return if (this.importance > otherConflict.importance) this + else otherConflict + + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/ExcludeGroup.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/ExcludeGroup.kt new file mode 100644 index 0000000..d752db5 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/ExcludeGroup.kt @@ -0,0 +1,69 @@ +package xyz.alexcrea.cuanvil.group + +import org.bukkit.NamespacedKey +import java.util.* + +class ExcludeGroup(name: String) : AbstractMaterialGroup(name) { + + override fun createDefaultSet(): MutableSet { + return NegativeMaterialSet() + } + + private var includedGroup: MutableSet = HashSet() + private val groupItems by lazy { createDefaultSet() } + + override fun isReferencing(other: AbstractMaterialGroup): Boolean { + for (materialGroup in includedGroup.iterator()) { + if ((materialGroup == other) || (materialGroup.isReferencing(other))) { + return true + } + } + return false + } + + override fun addToPolicy(type: NamespacedKey): ExcludeGroup { + includedMaterial.remove(type) + groupItems.remove(type) + + return this + } + + override fun addToPolicy(other: AbstractMaterialGroup): ExcludeGroup { + includedGroup.add(other) + groupItems.removeAll(other.getMaterials()) + + return this + } + + override fun setGroups(groups: MutableSet) { + groupItems.clear() + groupItems.addAll(includedMaterial) + + includedGroup.clear() + groups.forEach { group -> + if (!group.isReferencing(this)) { + includedGroup.add(group) + groupItems.removeAll(group.getMaterials()) + } + } + } + + override fun getGroups(): MutableSet { + return includedGroup + } + + override fun updateMaterials() { + groupItems.clear() + groupItems.addAll(includedMaterial) + + includedGroup.forEach { group -> + groupItems.addAll(group.getMaterials()) + } + } + + override fun getMaterials(): MutableSet { + return groupItems + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/IncludeGroup.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/IncludeGroup.kt new file mode 100644 index 0000000..fc9614b --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/IncludeGroup.kt @@ -0,0 +1,75 @@ +package xyz.alexcrea.cuanvil.group + +import org.bukkit.Material +import org.bukkit.NamespacedKey +import java.util.* + +class IncludeGroup(name: String) : AbstractMaterialGroup(name) { + override fun createDefaultSet(): MutableSet { + return HashSet() + } + + private var includedGroup: MutableSet = HashSet() + private val groupItems by lazy { createDefaultSet() } + + override fun isReferencing(other: AbstractMaterialGroup): Boolean { + for (materialGroup in includedGroup.iterator()) { + if ((materialGroup == other) || (materialGroup.isReferencing(other))) { + return true + } + } + return false + } + + override fun addToPolicy(type: NamespacedKey): IncludeGroup { + includedMaterial.add(type) + groupItems.add(type) + + return this + } + + override fun addToPolicy(other: AbstractMaterialGroup): IncludeGroup { + includedGroup.add(other) + groupItems.addAll(other.getMaterials()) + + return this + } + + override fun setGroups(groups: MutableSet) { + groupItems.clear() + groupItems.addAll(includedMaterial) + + includedGroup.clear() + groups.forEach { group -> + if (!group.isReferencing(this)) { + includedGroup.add(group) + groupItems.addAll(group.getMaterials()) + } + } + } + + override fun setNonGroupInheritedMaterials(materials: Set) { + super.setNonGroupInheritedMaterials(materials) + + updateMaterials() + } + + override fun getGroups(): MutableSet { + return includedGroup + } + + override fun updateMaterials() { + groupItems.clear() + groupItems.addAll(includedMaterial) + + includedGroup.forEach { group -> + groupItems.addAll(group.getMaterials()) + } + } + + override fun getMaterials(): MutableSet { + return groupItems + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/group/ItemGroupManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/ItemGroupManager.kt similarity index 50% rename from src/main/kotlin/xyz/alexcrea/group/ItemGroupManager.kt rename to src/main/kotlin/xyz/alexcrea/cuanvil/group/ItemGroupManager.kt index 7c02d47..348d0ff 100644 --- a/src/main/kotlin/xyz/alexcrea/group/ItemGroupManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/ItemGroupManager.kt @@ -1,54 +1,69 @@ -package xyz.alexcrea.group +package xyz.alexcrea.cuanvil.group -import io.delilaheve.UnsafeEnchants +import io.delilaheve.CustomAnvil import org.bukkit.Material import org.bukkit.configuration.ConfigurationSection -import org.bukkit.configuration.file.YamlConfiguration import java.util.* -import kotlin.collections.HashMap class ItemGroupManager { companion object { // Path for group type - private const val GROUP_TYPE_PATH = "type" + const val GROUP_TYPE_PATH = "type" + // Path for included items list - private const val MATERIAL_LIST_PATH = "items" + const val MATERIAL_LIST_PATH = "items" + // Path for included groups list - private const val GROUP_LIST_PATH = "groups" + const val GROUP_LIST_PATH = "groups" + // Temporary list of elements in default config that are use in future - private val FUTURE_MATERIAL = setOf("PIGLIN_HEAD","BRUSH") + private val FUTURE_MATERIAL = setOf("PIGLIN_HEAD", "BRUSH") } - private lateinit var groupMap : HashMap + lateinit var groupMap: LinkedHashMap // Read and create material groups - fun prepareGroups(config: YamlConfiguration){ - groupMap = HashMap() + fun prepareGroups(config: ConfigurationSection) { + groupMap = LinkedHashMap() val keys = config.getKeys(false) for (key in keys) { - if(groupMap.containsKey(key)) + if (groupMap.containsKey(key)) + continue + if (!config.isConfigurationSection(key)) continue createGroup(config, keys, key) } } + // Create group with existing groups + fun createGroup( + config: ConfigurationSection, + name: String + ): AbstractMaterialGroup{ + return createGroup(config, groupMap.keys, name) + } + + // Create group by key - private fun createGroup(config: YamlConfiguration, - keys: Set, - key: String): AbstractMaterialGroup{ + private fun createGroup( + config: ConfigurationSection, + keys: Set, + key: String + ): AbstractMaterialGroup { val groupSection = config.getConfigurationSection(key)!! - val groupType = groupSection.getString(GROUP_TYPE_PATH,null) + + val groupType = groupSection.getString(GROUP_TYPE_PATH, null) // Create Material group according to the group type val group: AbstractMaterialGroup - if(GroupType.EXCLUDE.equal(groupType)){ + if (groupType != null && GroupType.EXCLUDE.equal(groupType)) { group = ExcludeGroup(key) - }else { + } else { group = IncludeGroup(key) - if(!GroupType.INCLUDE.equal(groupType)){ - UnsafeEnchants.instance.logger.warning("Group $key have an invalid group type. default to Include.") + if (!GroupType.INCLUDE.equal(groupType)) { + CustomAnvil.instance.logger.warning("Group $key have an invalid group type. default to Include.") } } @@ -58,48 +73,56 @@ class ItemGroupManager { } // Read Group elements - private fun readGroup(group: AbstractMaterialGroup, - groupSection: ConfigurationSection, - config: YamlConfiguration, - keys: Set){ + private fun readGroup( + group: AbstractMaterialGroup, + groupSection: ConfigurationSection, + config: ConfigurationSection, + keys: Set + ) { // Read material to include in this group policy val materialList = groupSection.getStringList(MATERIAL_LIST_PATH) for (materialTemp in materialList) { val materialName = materialTemp.uppercase(Locale.getDefault()) val material = Material.getMaterial(materialName) - if(material == null){ + if (material == null) { // Check if we should warn the user - if(materialName !in FUTURE_MATERIAL){ - UnsafeEnchants.instance.logger.warning( - "Unknown material $materialTemp on group ${group.getName()}") + if (materialName !in FUTURE_MATERIAL) { + CustomAnvil.instance.logger.warning( + "Unknown material $materialTemp on group ${group.getName()}" + ) } continue } - group.addToPolicy(material) + group.addToPolicy(material.key) } // Read group to include in this group policy. // please note the group name is case-sensitive. val groupList = groupSection.getStringList(GROUP_LIST_PATH) for (groupName in groupList) { - if(groupName !in keys){ - UnsafeEnchants.instance.logger.warning( - "Group $groupName do not exist but is included in group ${group.getName()}") + if (groupName !in keys) { + CustomAnvil.instance.logger.warning( + "Group $groupName do not exist but is included in group ${group.getName()}" + ) continue } // Get other group or create it if not yet created - val otherGroup = if(!groupMap.containsKey(groupName)){ - createGroup(config,keys,groupName) - }else{ - groupMap[groupName]!! + val otherGroup = + if (!groupMap.containsKey(groupName)) { + if(!config.isConfigurationSection(groupName)) continue + createGroup(config, keys, groupName) } + else groupMap[groupName]!! + // Avoid self reference or it will create an infinite loop - if(otherGroup.isReferencing(group)){ - UnsafeEnchants.instance.logger.warning( - "Group $groupName is on a reference loop with group ${group.getName()} !") - UnsafeEnchants.instance.logger.warning( - "Please fix it in your item_groups config or the plugin will probably not work as expected.") + if (otherGroup.isReferencing(group)) { + CustomAnvil.instance.logger.warning( + "Group $groupName is on a reference loop with group ${group.getName()} !" + ) + CustomAnvil.instance.logger.warning( + "Please fix it in your item_groups config or the plugin will probably not work as expected." + ) continue } @@ -115,17 +138,17 @@ class ItemGroupManager { } -enum class GroupType(private val groupID: String) { +enum class GroupType(val groupID: String) { INCLUDE("include"), EXCLUDE("exclude") ; // Test if string is equal to the groupID of this enum - fun equal(toTest: String?): Boolean{ - if(toTest == null) + fun equal(toTest: String?): Boolean { + if (toTest == null) return false - return groupID.contentEquals(toTest.lowercase(Locale.getDefault())) + return groupID.contentEquals(toTest.lowercase(Locale.getDefault())) } } \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/NegativeMaterialSet.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/NegativeMaterialSet.kt new file mode 100644 index 0000000..d87004d --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/NegativeMaterialSet.kt @@ -0,0 +1,22 @@ +package xyz.alexcrea.cuanvil.group + +import org.bukkit.NamespacedKey +import xyz.alexcrea.cuanvil.util.MaterialUtil +import xyz.alexcrea.cuanvil.util.NegativeSet + +class NegativeMaterialSet: NegativeSet() { + + override fun iterator(): MutableIterator { + val materials = MaterialUtil.getMaterials() + materials.removeIf { negate.contains(it) } + + return materials.iterator() + } + + override fun isEmpty(): Boolean { + return negate.size >= MaterialUtil.getMaterialCount() + } + + override val size get() = MaterialUtil.getMaterialCount() - negate.size + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilCloseListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilCloseListener.kt new file mode 100644 index 0000000..d707b56 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilCloseListener.kt @@ -0,0 +1,25 @@ +package xyz.alexcrea.cuanvil.listener + +import org.bukkit.GameMode +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.inventory.InventoryCloseEvent +import org.bukkit.inventory.AnvilInventory +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager +import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil + +class AnvilCloseListener(private val packetManager: PacketManager) : Listener { + + @EventHandler + fun onAnvilClose(event: InventoryCloseEvent){ + val player = event.player + if(event.inventory !is AnvilInventory) return + if(player is Player && GameMode.CREATIVE != player.gameMode){ + packetManager.setInstantBuild(player, false) + } + + AnvilRenameDialogUtil.anvilRenameDialog.closeInventory(player) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt new file mode 100644 index 0000000..e393dbd --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -0,0 +1,613 @@ +package xyz.alexcrea.cuanvil.listener + +import io.delilaheve.CustomAnvil +import io.delilaheve.util.ConfigOptions +import io.delilaheve.util.ItemUtil.canMergeWith +import org.bukkit.GameMode +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.event.Event +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.inventory.ClickType +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.inventory.AnvilInventory +import org.bukkit.inventory.InventoryView +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.BookMeta +import xyz.alexcrea.cuanvil.anvil.AnvilCost +import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic +import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.AnvilResult +import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.CustomCraftResult +import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.LoreEditResult +import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.UnitRepairResult +import xyz.alexcrea.cuanvil.dependency.DependencyManager +import xyz.alexcrea.cuanvil.dependency.economy.EconomyManager +import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil.setComponentDisplayName +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener.Companion.ANVIL_INPUT_LEFT +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener.Companion.ANVIL_INPUT_RIGHT +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener.Companion.ANVIL_OUTPUT_SLOT +import xyz.alexcrea.cuanvil.util.CustomRecipeUtil +import xyz.alexcrea.cuanvil.util.MiniMessageUtil +import xyz.alexcrea.cuanvil.util.anvil.AnvilLoreEditUtil +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil +import xyz.alexcrea.cuanvil.util.config.LoreEditType +import java.util.* +import java.util.concurrent.atomic.AtomicReference +import kotlin.math.min + +class AnvilResultListener : Listener { + + companion object { + // static slot container + private val NO_SLOT = SlotContainer(SlotType.NO_SLOT, 0) + private val CURSOR_SLOT = SlotContainer(SlotType.CURSOR, 0) + } + + /** + * Event handler logic for when a player is trying to pull an item out of the anvil + */ + @EventHandler(ignoreCancelled = true) + fun anvilExtractionCheck(event: InventoryClickEvent) { + val player = event.whoClicked as? Player ?: return + val inventory = event.inventory as? AnvilInventory ?: return + val view = event.view + + if (event.rawSlot != ANVIL_OUTPUT_SLOT) { + return + } + + // Test if the event should bypass custom anvil. + if (DependencyManager.tryClickAnvilResultBypass(event, inventory)) return + + if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return + + val output = inventory.getItem(ANVIL_OUTPUT_SLOT) ?: return + val leftItem = inventory.getItem(ANVIL_INPUT_LEFT) ?: return + val rightItem = inventory.getItem(ANVIL_INPUT_RIGHT) + + // Deny by default. allow if working + event.result = Event.Result.DENY + if (GameMode.CREATIVE != player.gameMode && inventory.repairCost >= inventory.maximumRepairCost) { + return + } + + // Test custom recipe + val customRecipeResult = AnvilMergeLogic.testCustomRecipe(view, inventory, player, leftItem, rightItem) + if (!customRecipeResult.isEmpty()) { + onCustomCraft( + event, player, inventory, + leftItem, rightItem, customRecipeResult + ) + return + } + + // Do not continue if there was no change + if ((output == inventory.getItem(ANVIL_INPUT_LEFT))) { + return + } + + // Rename + if (rightItem == null) { + val result = AnvilMergeLogic.doRenaming(view, inventory, player, leftItem) + if (result.isEmpty()) return + + extractAnvilResult( + event, player, inventory, + null, 0, + null, 0, + result + ) + return + } + + // Merge + val canMerge = leftItem.canMergeWith(rightItem) + if (canMerge) { + val result = AnvilMergeLogic.doMerge(view, inventory, player, leftItem, rightItem) + + extractAnvilResult( + event, player, inventory, + null, 0, + null, 0, + result + ) + return + } + + // Unit repair + val unitRepairResult = AnvilMergeLogic.testUnitRepair( + view, inventory, player, + leftItem, rightItem + ) + if (!unitRepairResult.isEmpty()) { + onUnitRepairExtract( + rightItem, event, player, inventory, + unitRepairResult + ) + return + } + + // For lore edit + val loreResult = AnvilMergeLogic.testLoreEdit(player, leftItem, rightItem) + if (!loreResult.isEmpty()) { + if (loreResult.type.isBook) + handleBookLoreEdit(event, inventory, player, leftItem, rightItem, loreResult) + else + handlePaperLoreEdit(event, inventory, player, leftItem, rightItem, loreResult) + return + } + } + + private fun onCustomCraft( + event: InventoryClickEvent, + player: Player, + inventory: AnvilInventory, + leftItem: ItemStack, + rightItem: ItemStack?, + result: CustomCraftResult, + ) { + val recipe = result.recipe!! + val rawCost = result.customCraftCost.rawCost + val finalCost = + if (recipe.removeExactLinearXp) rawCost + else AnvilXpUtil.calculateLevelForXp(rawCost) + + CustomAnvil.log( + "gamemode: ${player.gameMode != GameMode.CREATIVE}, " + + "cost: $finalCost, level: ${player.level}, " + + "result: ${player.totalExperience < finalCost} ${player.level < finalCost}" + ) + + if (player.gameMode != GameMode.CREATIVE) { + if (ConfigOptions.shouldUseMoney(player)) { + result.cost.isMonetary = true + if (!EconomyManager.economy!!.has(player, result.cost.asMonetaryCost())) return + } else if (recipe.removeExactLinearXp) { + val levelXp = AnvilXpUtil.calculateXpForLevel(player.level) + val delta = AnvilXpUtil.calculateXpForLevel(player.level + 1) - levelXp + val totalXp = levelXp + player.exp * delta + if (totalXp < finalCost) return + } else if (player.level < finalCost) return + } + + // We give the item manually + // But first we check if we should give the item + val slotDestination = getActionSlot(event, player) + if (slotDestination.type == SlotType.NO_SLOT) return + + // Handle not creative middle click... + if (event.click != ClickType.MIDDLE && + !handleCustomCraftClick( + event, + inventory, + player, + leftItem, + rightItem, + result + ) + ) return + + // Finally, we add the item to the player + if (slotDestination.type == SlotType.CURSOR) { + player.setItemOnCursor(result.item) + } else {// We assume SlotType == SlotType.INVENTORY + player.inventory.setItem(slotDestination.slot, result.item) + } + } + + private fun handleCustomCraftClick( + event: InventoryClickEvent, + inventory: AnvilInventory, player: Player, + leftItem: ItemStack, rightItem: ItemStack?, + result: CustomCraftResult + ): Boolean { + val amount = result.amount + val recipe = result.recipe!! + + // We remove what should be removed + if (rightItem != null) { + if (recipe.rightItem == null) return false// in case it changed + + rightItem.amount -= amount * recipe.rightItem!!.amount + inventory.setItem(ANVIL_INPUT_RIGHT, rightItem) + } + + leftItem.amount -= amount * recipe.leftItem!!.amount + inventory.setItem(ANVIL_INPUT_LEFT, leftItem) + + removeCustomCraftCost(player, result) + + // Then we try to find the new values for the anvil + val newAmount = CustomRecipeUtil.getCustomRecipeAmount(recipe, leftItem, rightItem) + + CustomAnvil.verboseLog("new amount is $newAmount") + if (newAmount <= 0 || recipe.exactCount) { + inventory.setItem(ANVIL_OUTPUT_SLOT, null) + } else { + val resultItem: ItemStack = recipe.resultItem!!.clone() + resultItem.amount *= newAmount + + val newXp = newAmount * newAmount + + inventory.repairCost = newXp + event.view.setProperty(InventoryView.Property.REPAIR_COST, newXp) + + inventory.setItem(ANVIL_OUTPUT_SLOT, resultItem) + + player.updateInventory() + } + return true + } + + private fun removeCustomCraftCost(player: Player, result: CustomCraftResult) { + if (player.gameMode == GameMode.CREATIVE) return + + val rawCost = result.customCraftCost.rawCost + if (result.cost.isMonetary) { + EconomyManager.economy!!.remove(player, result.cost.asMonetaryCost()) + return + } + + if (result.recipe!!.removeExactLinearXp) { + val levelXp = AnvilXpUtil.calculateXpForLevel(player.level) + val delta = AnvilXpUtil.calculateXpForLevel(player.level + 1) - levelXp + var totalXp = levelXp + player.exp * delta + totalXp -= rawCost + + val newLevel = AnvilXpUtil.calculateLevelForXp(totalXp.toInt()) + + val newLevelXp = AnvilXpUtil.calculateXpForLevel(newLevel) + val newDelta = AnvilXpUtil.calculateXpForLevel(newLevel + 1) - newLevelXp + val xp = (totalXp - newLevelXp) / newDelta + + player.level = newLevel + player.exp = xp / newDelta + } else { + player.level -= AnvilXpUtil.calculateLevelForXp(rawCost) + } + + } + + private fun tryRemoveCost(player: Player, result: AnvilResult): Boolean { + if (player.gameMode == GameMode.CREATIVE) return true + + val cost = result.cost + if (cost.isMonetary) { + val result = EconomyManager.economy!!.remove(player, cost.asMonetaryCost()) + if (!result) return false + } else { + val xpCost = cost.filteredXpCost() + if (xpCost > AnvilXpUtil.maximumXpCost(result.ignoreXpRules)) return false + if (player.level < xpCost) return false + + player.level -= xpCost + } + + return true + } + + private fun extractAnvilResult( + event: InventoryClickEvent, + player: Player, + inventory: AnvilInventory, + leftItem: ItemStack?, + leftRemoveCount: Int, + rightItem: ItemStack?, + rightRemoveCount: Int, + result: AnvilResult + ): Boolean { + if (result.isEmpty()) return false + + // To avoid vanilla, we cancel the event + event.result = Event.Result.DENY + event.isCancelled = true + val cost = result.cost + + processCost(inventory, player, cost) + if (!cost.valid && player.gameMode != GameMode.CREATIVE) return false + + // Where should we get the item + val slotDestination = getActionSlot(event, player) + if (slotDestination.type == SlotType.NO_SLOT) return false + + // If not creative middle click... + if (event.click != ClickType.MIDDLE) { + if (!tryRemoveCost(player, result)) return false + + // We remove what should be removed + if (leftItem != null) leftItem.amount -= leftRemoveCount + inventory.setItem(ANVIL_INPUT_LEFT, leftItem) + + if (rightItem != null) rightItem.amount -= rightRemoveCount + inventory.setItem(ANVIL_INPUT_RIGHT, rightItem) + + inventory.setItem(ANVIL_OUTPUT_SLOT, null) + + } + + // Finally, we add the item to the player + if (SlotType.CURSOR == slotDestination.type) { + player.setItemOnCursor(result.item) + } else {// We assume SlotType == SlotType.INVENTORY + player.inventory.setItem(slotDestination.slot, result.item) + } + + // TODO probably anvil damage & sound here ?? + return true + } + + private fun processCost(inventory: AnvilInventory, player: Player, cost: AnvilCost) { + var sum = cost.repair + + if ( + !ConfigOptions.doRemoveCostLimit && + ConfigOptions.doCapCost + ) { + val final = min(sum, ConfigOptions.maxAnvilCost) + cost.generic += (final - sum) + + sum = final + } + + if (ConfigOptions.shouldUseMoney(player)) { + cost.isMonetary = true + if (!EconomyManager.economy!!.has(player, cost.asMonetaryCost())) + cost.valid = false + } else { + if ((inventory.maximumRepairCost <= sum) + || (player.level < sum) + ) cost.valid = false + } + } + + private fun onUnitRepairExtract( + rightItem: ItemStack, + event: InventoryClickEvent, + player: Player, + inventory: AnvilInventory, + result: UnitRepairResult, + ) { + // We give the item manually + extractAnvilResult( + event, player, inventory, + null, 0, + rightItem, result.repairAmount, + result + ) + } + + private fun handleBookLoreEdit( + event: InventoryClickEvent, + inventory: AnvilInventory, + player: Player, + leftItem: ItemStack, + rightItem: ItemStack, + result: LoreEditResult + ) { + if (result.type.isAppend) + handleBookLoreAppend(event, inventory, player, rightItem, result) + else + handleBookLoreRemove(event, inventory, player, leftItem, rightItem, result) + } + + private fun handleBookLoreAppend( + event: InventoryClickEvent, + inventory: AnvilInventory, + player: Player, + rightItem: ItemStack, + result: LoreEditResult + ) { + val bookMeta = rightItem.itemMeta as BookMeta? ?: return + + // Remove pages to book + val clearedBook: ItemStack? + if (LoreEditType.APPEND_BOOK.doConsume) { + clearedBook = null + } else { + clearedBook = rightItem.clone() + bookMeta.pages = Collections.emptyList() + clearedBook.itemMeta = bookMeta + } + + extractAnvilResult( + event, player, inventory, + null, 0, + clearedBook, 0, + result + ) + } + + private fun handleBookLoreRemove( + event: InventoryClickEvent, + inventory: AnvilInventory, + player: Player, + leftItem: ItemStack, + rightItem: ItemStack, + result: LoreEditResult + ) { + val bookMeta = rightItem.itemMeta as BookMeta? ?: return + + // fill book meta + val lore = DependencyManager.stripLore(leftItem) + if (lore.isEmpty()) return + + val rightCopy: ItemStack? + if (LoreEditType.REMOVE_BOOK.doConsume) { + rightCopy = null + } else { + // Uncolor the page + AnvilLoreEditUtil.uncolorLines(player, lore, LoreEditType.REMOVE_BOOK) + + val bookPage = StringBuilder() + lore.forEach { + if (bookPage.isNotEmpty()) bookPage.append('\n') + if (it == null) return@forEach + + bookPage.append(MiniMessageUtil.plain_text_mm.serialize(it)) + } + + val resultPage = bookPage.toString() + //TODO maybe check page size ? bc it may be too big ??? + + rightCopy = rightItem.clone() + bookMeta.setPages(resultPage) + rightCopy.itemMeta = bookMeta + } + + extractAnvilResult( + event, player, inventory, + null, 0, + rightCopy, 0, + result + ) + } + + private fun handlePaperLoreEdit( + event: InventoryClickEvent, + inventory: AnvilInventory, + player: Player, + leftItem: ItemStack, + rightItem: ItemStack, + result: LoreEditResult + ) { + if (result.type.isAppend) + handlePaperLoreAppend(event, inventory, player, rightItem, result) + else + handlePaperLoreRemove(event, inventory, player, leftItem, rightItem, result) + } + + private fun handlePaperLoreAppend( + event: InventoryClickEvent, + inventory: AnvilInventory, + player: Player, + rightItem: ItemStack, + result: LoreEditResult + ) { + val paperMeta = rightItem.itemMeta ?: return + + + val paperCopy: ItemStack? + if (LoreEditType.APPEND_PAPER.doConsume) { + paperCopy = null + } else { + // Remove custom name to paper + paperCopy = rightItem.clone() + paperCopy.amount = 1 + paperMeta.setComponentDisplayName(null) + + // Remove pcd name + AnvilMergeLogic.processPCD(paperMeta, player, null) + + paperCopy.itemMeta = paperMeta + } + + if (rightItem.amount > 1) { + extractAnvilResult( + event, player, inventory, + paperCopy, 0, + rightItem, 1, + result + ) + } else { + extractAnvilResult( + event, player, inventory, + null, 0, + paperCopy, 0, + result + ) + } + } + + private fun handlePaperLoreRemove( + event: InventoryClickEvent, + inventory: AnvilInventory, + player: Player, + leftItem: ItemStack, + rightItem: ItemStack, + result: LoreEditResult + ) { + val leftMeta = leftItem.itemMeta + if (leftMeta == null || !leftMeta.hasLore()) return + + val lore = DependencyManager.stripLore(leftItem) + if (lore.isEmpty()) return + + // Create result item + val rightClone: ItemStack? + if (LoreEditType.REMOVE_PAPER.doConsume) { + rightClone = null + } else { + val removeEnd = LoreEditConfigUtil.paperLoreOrderIsEnd + val line = if (removeEnd) lore[lore.size - 1] + else lore[0] + + // uncolor the line + val ref = AtomicReference(line) + AnvilLoreEditUtil.uncolorLine(player, ref, LoreEditType.REMOVE_PAPER) + + rightClone = rightItem.clone() + rightClone.amount = 1 + + val resultMeta = rightClone.itemMeta ?: return + resultMeta.setComponentDisplayName(ref.get()) + rightClone.itemMeta = resultMeta + } + + if (rightItem.amount > 1) { + extractAnvilResult( + event, player, inventory, + rightClone, 0, + rightItem, 1, + result + ) + } else { + extractAnvilResult( + event, player, inventory, + null, 0, + rightClone, 0, + result + ) + } + } + + /** + * Get the destination slot or "NO_SLOT" slot container if there is no slot available + */ + private fun getActionSlot(event: InventoryClickEvent, player: Player): SlotContainer { + if (event.isShiftClick) { + val inventory = player.inventory + val firstEmpty = inventory.firstEmpty() + if (firstEmpty == -1) { + return NO_SLOT + } + //check hotbare full + var slotIndex = 8 + while (slotIndex >= 0 && ((inventory.getItem(slotIndex)?.type ?: Material.AIR) != Material.AIR)) { + slotIndex-- + } + if (slotIndex >= 0) { + return SlotContainer(SlotType.INVENTORY, slotIndex) + } + slotIndex = 35 //4*9 - 1 (max of player inventory) + while (slotIndex >= 9 && ((inventory.getItem(slotIndex)?.type ?: Material.AIR) != Material.AIR)) { + slotIndex-- + } + if (slotIndex < 9) { + return NO_SLOT + } + return SlotContainer(SlotType.INVENTORY, slotIndex) + } else if (player.itemOnCursor.type != Material.AIR) return NO_SLOT + return CURSOR_SLOT + } + + private class SlotContainer(val type: SlotType, val slot: Int) + private enum class SlotType { + CURSOR, + INVENTORY, + NO_SLOT + + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/ChatEventListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/ChatEventListener.kt new file mode 100644 index 0000000..f166fa3 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/ChatEventListener.kt @@ -0,0 +1,49 @@ +package xyz.alexcrea.cuanvil.listener + +import io.delilaheve.CustomAnvil +import org.bukkit.entity.HumanEntity +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.player.AsyncPlayerChatEvent +import org.bukkit.event.player.PlayerQuitEvent +import xyz.alexcrea.cuanvil.dependency.DependencyManager +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.function.Consumer + +class ChatEventListener : Listener { + + private val playerListenMap: ConcurrentHashMap> = ConcurrentHashMap() + + fun setListenedCallback(playeruuid: UUID, callback: Consumer) { + playerListenMap[playeruuid] = callback + } + + fun setListenedCallback(player: HumanEntity, callback: Consumer) { + setListenedCallback(player.uniqueId, callback) + } + + @EventHandler + fun onQuit(event: PlayerQuitEvent) { + val eventCallback = playerListenMap.remove(event.player.uniqueId) ?: return + eventCallback.accept(null) + + } + + @EventHandler + fun onChat(event: AsyncPlayerChatEvent) { + if (event.isCancelled) return + val player = event.player + val eventCallback = playerListenMap.remove(player.uniqueId) ?: return + + event.isCancelled = true + + // sync callback with default server thread + DependencyManager.scheduler.scheduleOnEntity( + CustomAnvil.instance, player, + Runnable { + eventCallback.accept(event.message) + }, 0L) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt new file mode 100644 index 0000000..0217983 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -0,0 +1,181 @@ +package xyz.alexcrea.cuanvil.listener + +import com.github.stefvanschie.inventoryframework.util.InventoryViewUtil +import io.delilaheve.CustomAnvil +import io.delilaheve.util.ConfigOptions +import io.delilaheve.util.ItemUtil.canMergeWith +import org.bukkit.entity.HumanEntity +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.inventory.PrepareAnvilEvent +import org.bukkit.inventory.AnvilInventory +import org.bukkit.inventory.InventoryView +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.EnchantmentStorageMeta +import org.bukkit.inventory.meta.ItemMeta +import xyz.alexcrea.cuanvil.anvil.AnvilCost +import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.AnvilResult +import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.doMerge +import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.doRenaming +import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.testCustomRecipe +import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.testLoreEdit +import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.testUnitRepair +import xyz.alexcrea.cuanvil.dependency.DependencyManager +import xyz.alexcrea.cuanvil.util.MaterialUtil.isAir +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil + +/** + * Listener for anvil events + */ +class PrepareAnvilListener : Listener { + + companion object { + + // Anvil's output slot + const val ANVIL_INPUT_LEFT = 0 + const val ANVIL_INPUT_RIGHT = 1 + const val ANVIL_OUTPUT_SLOT = 2 + + var IS_EMPTY_TEST = false + } + + /** + * Event handler logic for when an anvil contains items to be combined + */ + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + fun anvilCombineCheck(event: PrepareAnvilEvent) { + val view = event.view + val inventory = event.inventory + + val player = InventoryViewUtil.getInstance().getPlayer(view) + if(player !is Player) return + + tryRenameDialog(player, event) + + // Test if custom anvil is bypassed before immutability test + if (DependencyManager.earlyTryEventPreAnvilBypass(event, player)) { + // even if we got bypassed we still want to set price + AnvilXpUtil.setAnvilInvCost(inventory, view, player, AnvilCost(event.inventory.repairCost)) + return + } + + val first = inventory.getItem(ANVIL_INPUT_LEFT) + val second = inventory.getItem(ANVIL_INPUT_RIGHT) + + if(IS_EMPTY_TEST) { + IS_EMPTY_TEST = false + applyResult(event, player, AnvilResult.EMPTY) + return + } + + if (ConfigOptions.verboseDebugLog) { + CustomAnvil.verboseLog("Testing items:") + CustomAnvil.verboseLog("first: $first") + CustomAnvil.verboseLog("second: $second") + } + if (isImmutable(first) || isImmutable(second)) { + CustomAnvil.verboseLog("Skipping anvil process as one of the two item is immutable") + + applyResult(event, player, AnvilResult.EMPTY) + return + } + + // Test if the event should bypass custom anvil. + if (DependencyManager.tryEventPreAnvilBypass(event, player)) { + // even if we got bypassed we still want to set price + AnvilXpUtil.setAnvilInvCost(inventory, view, player, AnvilCost(event.inventory.repairCost)) + return + } + + if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return + + val result = getResult(view, inventory, player, first, second) + applyResult(event, player, result) + } + + fun getResult( + view: InventoryView, //TODO use anvil view + inventory: AnvilInventory, + player: Player, + first: ItemStack?, second: ItemStack?) : AnvilResult + { + if(first == null) + return AnvilResult.EMPTY + + // Test custom recipe + var result: AnvilResult = testCustomRecipe(view, inventory, player, first, second) + if (!result.isEmpty()) + return result + + // Test rename lonely item + val shouldTryRename = second.isAir + CustomAnvil.verboseLog("checking air in main logic: $shouldTryRename") + if (shouldTryRename) + return doRenaming(view, inventory, player, first) + + // Test for merge + if (first.canMergeWith(second!!)) + return doMerge(view, inventory, player, first, second) + + // Test for unit repair + result = testUnitRepair(view, inventory, player, first, second) + if (!result.isEmpty()) + return result + + // Test for lore edit + result = testLoreEdit(player, first, second) + if (!result.isEmpty()) + return result + + return AnvilResult.EMPTY + } + + private fun tryRenameDialog( + player: HumanEntity, + event: PrepareAnvilEvent + ) { + if(!ConfigOptions.canUseDialogRename(player)) return + + AnvilRenameDialogUtil.anvilRenameDialog.tryShowDialog(player, event) + } + + private fun isImmutable(item: ItemStack?): Boolean { + if (item.isAir) return false + + val meta = item!!.itemMeta + return meta != null && + (hasImmutableEnchants(meta) || hasImmutableStoredEnchants(meta)) + } + + private fun hasImmutableEnchants(meta: ItemMeta): Boolean { + if (!meta.hasEnchants()) return false + + for (enchant in meta.enchants.keys) { + if (ConfigOptions.isImmutable(enchant.key)) return true + } + return false + } + + private fun hasImmutableStoredEnchants(meta: ItemMeta): Boolean { + if (meta !is EnchantmentStorageMeta || !meta.hasStoredEnchants()) return false + + for (enchant in meta.storedEnchants.keys) { + if (ConfigOptions.isImmutable(enchant.key)) return true + } + return false + } + + private fun applyResult(event: PrepareAnvilEvent, player: Player, result: AnvilResult) { + event.result = result.item + + if(result.item == null) { + AnvilXpUtil.onNoResult(player, event.view) + return + } + AnvilXpUtil.setAnvilInvCost(event.inventory, event.view, player, result.cost, result.ignoreXpRules) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/AnvilCustomRecipe.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/AnvilCustomRecipe.kt new file mode 100644 index 0000000..fd079c3 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/AnvilCustomRecipe.kt @@ -0,0 +1,214 @@ +package xyz.alexcrea.cuanvil.recipe + +import io.delilaheve.CustomAnvil +import org.bukkit.configuration.ConfigurationSection +import org.bukkit.inventory.ItemStack +import xyz.alexcrea.cuanvil.anvil.AnvilUseType +import xyz.alexcrea.cuanvil.config.ConfigHolder +import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant +import xyz.alexcrea.cuanvil.util.MaterialUtil.isAir +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil + +class AnvilCustomRecipe( + val name: String, + var exactCount: Boolean, + //var exactLeft: Boolean, + //var exactRight: Boolean, + + var levelCostPerCraft: Int, + + var XpCostPerCraft: Int, + var removeExactLinearXp: Boolean, + + var leftItem: ItemStack?, + var rightItem: ItemStack?, + var resultItem: ItemStack?, +) { + + // Static config name + companion object { + const val EXACT_COUNT_CONFIG = "exact_count" + //const val EXACT_LEFT_CONFIG = "exact_left" + //const val EXACT_RIGHT_CONFIG = "exact_right" + + const val XP_LEVEL_COST_CONFIG = "xp_cost" + const val LINEAR_XP_COST_CONFIG = "linear_xp_cost" + const val REMOVE_EXACT_XP_CONFIG = "remove_exact_linear_xp" + + const val LEFT_ITEM_CONFIG = "left_item" + const val RIGHT_ITEM_CONFIG = "right_item" + const val RESULT_ITEM_CONFIG = "result_item" + + + const val DEFAULT_EXACT_COUNT_CONFIG = true + //val DEFAULT_EXACT_LEFT_CONFIG = true + //val DEFAULT_EXACT_RIGHT_CONFIG = true + + const val DEFAULT_XP_LEVEL_COST_CONFIG = 1 + const val DEFAULT_LINEAR_XP_COST_CONFIG = 0 + const val DEFAULT_REMOVE_EXACT_XP_CONFIG = false + + val DEFAULT_LEFT_ITEM_CONFIG: ItemStack? = null + val DEFAULT_RIGHT_ITEM_CONFIG: ItemStack? = null + val DEFAULT_RESULT_ITEM_CONFIG: ItemStack? = null + + val XP_COST_CONFIG_RANGE = 0..255 + + fun getFromConfig(name: String, configSection: ConfigurationSection?): AnvilCustomRecipe? { + if (configSection == null) return null + return AnvilCustomRecipe( + name, + configSection.getBoolean(EXACT_COUNT_CONFIG, DEFAULT_EXACT_COUNT_CONFIG), + //configSection.getBoolean(EXACT_LEFT_CONFIG, true), + //configSection.getBoolean(EXACT_RIGHT_CONFIG, true), + + configSection.getInt(XP_LEVEL_COST_CONFIG, DEFAULT_XP_LEVEL_COST_CONFIG), + configSection.getInt(LINEAR_XP_COST_CONFIG, DEFAULT_LINEAR_XP_COST_CONFIG), + configSection.getBoolean(REMOVE_EXACT_XP_CONFIG, DEFAULT_REMOVE_EXACT_XP_CONFIG), + + + configSection.getItemStack(LEFT_ITEM_CONFIG, DEFAULT_LEFT_ITEM_CONFIG), + + configSection.getItemStack(RIGHT_ITEM_CONFIG, DEFAULT_RIGHT_ITEM_CONFIG), + configSection.getItemStack(RESULT_ITEM_CONFIG, DEFAULT_RESULT_ITEM_CONFIG), + + ) + } + + fun getFromConfig(name: String): AnvilCustomRecipe? { + return getFromConfig(name, ConfigHolder.CUSTOM_RECIPE_HOLDER.config.getConfigurationSection(name)) + } + } + + fun validate(): Boolean { + return !leftItem.isAir && + (rightItem == null || !resultItem.isAir) && + !resultItem.isAir + } + + fun saveToFile(writeFile: Boolean, doBackup: Boolean) { + val fileConfig = ConfigHolder.CUSTOM_RECIPE_HOLDER.config + + fileConfig["$name.$EXACT_COUNT_CONFIG"] = exactCount + //fileConfig.set("$name.$EXACT_LEFT_CONFIG", exactLeft) + //fileConfig.set("$name.$EXACT_RIGHT_CONFIG", exactRight) + + fileConfig["$name.$XP_LEVEL_COST_CONFIG"] = levelCostPerCraft + fileConfig["$name.$LINEAR_XP_COST_CONFIG"] = XpCostPerCraft + fileConfig["$name.$REMOVE_EXACT_XP_CONFIG"] = removeExactLinearXp + + fileConfig["$name.$LEFT_ITEM_CONFIG"] = leftItem + fileConfig["$name.$RIGHT_ITEM_CONFIG"] = rightItem + fileConfig["$name.$RESULT_ITEM_CONFIG"] = resultItem + + + if (writeFile) { + ConfigHolder.CUSTOM_RECIPE_HOLDER.saveToDisk(doBackup) + } + } + + @Deprecated("Should use saveToFile(Boolean, Boolean) instead") //TODO determine when an where to save/do backup and remove use of variable like TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE + fun saveToFile() { + saveToFile( + GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE, + GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE + ) + } + + fun updateFromFile() { + this.exactCount = ConfigHolder.CUSTOM_RECIPE_HOLDER.config.getBoolean( + "$name.$EXACT_COUNT_CONFIG", + DEFAULT_EXACT_COUNT_CONFIG + ) + + this.levelCostPerCraft = ConfigHolder.CUSTOM_RECIPE_HOLDER.config.getInt( + "$name.$XP_LEVEL_COST_CONFIG", + DEFAULT_XP_LEVEL_COST_CONFIG + ) + + this.XpCostPerCraft = ConfigHolder.CUSTOM_RECIPE_HOLDER.config.getInt( + "$name.$LINEAR_XP_COST_CONFIG", + DEFAULT_LINEAR_XP_COST_CONFIG + ) + + this.removeExactLinearXp = ConfigHolder.CUSTOM_RECIPE_HOLDER.config.getBoolean( + "$name.$REMOVE_EXACT_XP_CONFIG", + DEFAULT_REMOVE_EXACT_XP_CONFIG + ) + + // Update items + val leftItem = ConfigHolder.CUSTOM_RECIPE_HOLDER.config.getItemStack( + "$name.$LEFT_ITEM_CONFIG", + DEFAULT_LEFT_ITEM_CONFIG + ) + + this.rightItem = ConfigHolder.CUSTOM_RECIPE_HOLDER.config.getItemStack( + "$name.$RIGHT_ITEM_CONFIG", + DEFAULT_RIGHT_ITEM_CONFIG + ) + + this.resultItem = ConfigHolder.CUSTOM_RECIPE_HOLDER.config.getItemStack( + "$name.$RESULT_ITEM_CONFIG", + DEFAULT_RESULT_ITEM_CONFIG + ) + + // Update material map + ConfigHolder.CUSTOM_RECIPE_HOLDER.recipeManager.cleanSetLeftItem(this, leftItem) + + } + + fun testItem(item1: ItemStack, item2: ItemStack?): Boolean { + CustomAnvil.verboseLog("Testing $name $leftItem") + // We assume this function can be call only if leftItem != null + + // Test if valid + if (!validate()) return false + + val leftSimilar = leftItem!!.isSimilar(item1) + CustomAnvil.verboseLog("Validated test !") + + // test of left item + if (!leftSimilar) return false // Test similar + if (exactCount) { + if ((leftItem!!.amount != item1.amount)) return false // test exact amount + } else if (item1.amount < leftItem!!.amount) return false // test if it has at least the amount we ask + + CustomAnvil.verboseLog("Left item passed !") + + // we don't know if right item can be + if (rightItem == null) { // null test + if (item2 != null) return false + } else { + val rightSimilar = rightItem!!.isSimilar(item2) + CustomAnvil.verboseLog("Right similar: $rightSimilar") + if (!rightSimilar) return false // test if similar when not null + + if (exactCount) { + if (rightItem!!.amount != item2!!.amount) return false // test exact amount + } else if (item2!!.amount < rightItem!!.amount) return false // test if it has at least the amount we ask + } + + CustomAnvil.verboseLog("Right item passed !") + + return true + } + + override fun toString(): String { + return name + } + + fun determineCost(amount: Int, first: ItemStack, resultItem: ItemStack): Int { + // First we determine the non linear level cost + var levelCost = levelCostPerCraft * amount + // TODO Maybe add an option per custom craft to ignore/not ignore penalty ?? + levelCost += AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.CUSTOM_CRAFT) + + var xpCost = AnvilXpUtil.calculateXpForLevel(levelCost) + // Then we add the linear cost + xpCost += XpCostPerCraft * amount + + return xpCost + } + + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/CustomAnvilRecipeManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/CustomAnvilRecipeManager.kt new file mode 100644 index 0000000..f271d90 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/CustomAnvilRecipeManager.kt @@ -0,0 +1,78 @@ +package xyz.alexcrea.cuanvil.recipe + +import io.delilaheve.CustomAnvil +import org.bukkit.Material +import org.bukkit.configuration.file.FileConfiguration +import org.bukkit.inventory.ItemStack + +class CustomAnvilRecipeManager { + + lateinit var recipeList: ArrayList + + lateinit var recipeByMat: HashMap> + + fun prepareRecipes(config: FileConfiguration) { + recipeList = ArrayList() + recipeByMat = HashMap() + + // read all configs + val keys = config.getKeys(false) + for (key in keys) { + val recipe = AnvilCustomRecipe.getFromConfig(key) + if (recipe == null) { + CustomAnvil.log("Can't load recipe $key") + continue + } + + cleanAddNew(recipe) + } + + } + + + fun cleanAddNew(recipe: AnvilCustomRecipe) { + recipeList.add(recipe) + val leftItem = recipe.leftItem + if (leftItem != null) { + addToMatMap(recipe, leftItem) + } + + } + + fun cleanSetLeftItem(recipe: AnvilCustomRecipe, leftItem: ItemStack?) { + // Remove left item mat if exist + val oldLeftItem = recipe.leftItem + if (oldLeftItem != null) { + val oldMat = oldLeftItem.type + + val test = recipeByMat[oldMat] + test!!.remove(recipe) + } + if (leftItem != null) { + addToMatMap(recipe, leftItem) + } + + recipe.leftItem = leftItem + } + + private fun addToMatMap(recipe: AnvilCustomRecipe, leftItem: ItemStack) { + var recipeList = recipeByMat[leftItem.type] + if (recipeList == null) { + recipeList = ArrayList() + recipeByMat[leftItem.type] = recipeList + } + recipeList.add(recipe) + + } + + fun cleanRemove(recipe: AnvilCustomRecipe): Boolean { + + val exist = recipeList.remove(recipe) + if (exist) { + cleanSetLeftItem(recipe, null) + } + + return exist; + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/CustomRecipeUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/CustomRecipeUtil.kt new file mode 100644 index 0000000..058b25a --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/CustomRecipeUtil.kt @@ -0,0 +1,54 @@ +package xyz.alexcrea.cuanvil.util + +import io.delilaheve.CustomAnvil +import org.bukkit.inventory.ItemStack +import xyz.alexcrea.cuanvil.config.ConfigHolder +import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe +import kotlin.math.min + +object CustomRecipeUtil { + + fun getCustomRecipe ( + leftItem: ItemStack, + rightItem: ItemStack?) : AnvilCustomRecipe? { + + val recipeList = ConfigHolder.CUSTOM_RECIPE_HOLDER.recipeManager.recipeByMat[leftItem.type] ?: return null + + CustomAnvil.verboseLog("Testing " + recipeList.size + " recipe...") + for (recipe in recipeList) { + if(recipe.testItem(leftItem, rightItem)){ + return recipe + } + } + + return null + } + + fun getCustomRecipeAmount( + recipe: AnvilCustomRecipe, + leftItem: ItemStack, + rightItem: ItemStack? + ): Int{ + return if(recipe.exactCount) { + if(leftItem.amount != recipe.leftItem!!.amount){ + 0 + }else if(rightItem != null && rightItem.amount != recipe.rightItem!!.amount){ + 0 + }else{ + 1 + } + } + else { + // test amount + val resultItem = recipe.resultItem!! // we know exist as the recipe was returned to us + val maxResultAmount = resultItem.maxStackSize/resultItem.amount + val maxLeftAmount = leftItem.amount/recipe.leftItem!!.amount + val maxRightAmount = if(rightItem == null){ maxLeftAmount } else{ rightItem.amount/recipe.rightItem!!.amount } + + CustomAnvil.verboseLog("resultItem: $resultItem, maxResultAmount: $maxResultAmount, maxLeftAmount: $maxLeftAmount, maxRightAmount: $maxRightAmount") + + min(min(maxResultAmount, maxLeftAmount), maxRightAmount) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MaterialUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MaterialUtil.kt new file mode 100644 index 0000000..6b55662 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MaterialUtil.kt @@ -0,0 +1,105 @@ +package xyz.alexcrea.cuanvil.util + +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.inventory.ItemStack +import xyz.alexcrea.cuanvil.dependency.DependencyManager +import xyz.alexcrea.cuanvil.dependency.plugins.EcoItemDependencyUtil + +object MaterialUtil { + + val ItemStack?.isAir: Boolean + get() { + return this == null || this.type.isAir || this.amount == 0 + } + + val NamespacedKey?.isAir: Boolean + get() { + return Material.AIR.key == this + } + + val ItemStack.customType: NamespacedKey + get() { + if(DependencyManager.ecoEnchantCompatibility != null) { + val result = EcoItemDependencyUtil.ecoItemNamespace(this) + if(result != null) return result + } + + val itemAdder = DependencyManager.itemsAdderCompatibility + if(itemAdder != null) { + val result = itemAdder.getKey(this) + if (result != null) return result + } + + return this.type.key + } + + private fun bukkitMaterialFromKey(key: NamespacedKey): Material? { + //TODO on paper only transition Registry.MATERIAL.get(key) + return Material.matchMaterial(key.toString()) + } + + fun getMatFromKey(key: NamespacedKey): Material? { + if(DependencyManager.ecoEnchantCompatibility != null) { + val result = EcoItemDependencyUtil.ecoItemMaterialFromKey(key) + if(result != null) return result + } + + val itemAdder = DependencyManager.itemsAdderCompatibility + if(itemAdder != null) { + val result = itemAdder.fromKey(key) + if (result != null) return result.type + } + + return bukkitMaterialFromKey(key) + } + + fun itemFromKey(key: NamespacedKey): ItemStack { + if(DependencyManager.ecoEnchantCompatibility != null) { + val result = EcoItemDependencyUtil.newEcoItemstack(key) + if(result != null) return result + } + + val itemAdder = DependencyManager.itemsAdderCompatibility + if(itemAdder != null) { + val result = itemAdder.fromKey(key) + if (result != null) return result + } + + return ItemStack(bukkitMaterialFromKey(key)!!) + } + + fun materialExist(key: NamespacedKey): Boolean { + return getMatFromKey(key) != null + } + + fun getMaterialCount(): Int { + var count = Material.entries.size + if(DependencyManager.ecoEnchantCompatibility != null) { + count += EcoItemDependencyUtil.getItems().size + } + + val itemAdder = DependencyManager.itemsAdderCompatibility + if(itemAdder != null) { + count += itemAdder.idsCount().size + } + + return count + } + + fun getMaterials(): MutableList { + val all = ArrayList(Material.entries.map { it.key }) + if(DependencyManager.ecoEnchantCompatibility != null) { + all.addAll(EcoItemDependencyUtil.getItems()) + } + + val itemAdder = DependencyManager.itemsAdderCompatibility + if(itemAdder != null) { + all.addAll(itemAdder.idsCount().map { NamespacedKey.fromString(it) }) + } + + return all + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt new file mode 100644 index 0000000..1763db5 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt @@ -0,0 +1,97 @@ +package xyz.alexcrea.cuanvil.util + +import dev.faststats.ErrorTracker +import dev.faststats.bukkit.BukkitContext +import dev.faststats.data.Metric +import io.delilaheve.CustomAnvil +import io.delilaheve.util.ConfigOptions +import xyz.alexcrea.cuanvil.command.DiagnosticExecutor +import xyz.alexcrea.cuanvil.config.ConfigHolder +import xyz.alexcrea.cuanvil.dependency.DependencyManager + +object MetricsUtil { + + private const val BSTATS_PLUGIN_ID = 20923 + private const val FASTSTATS_TOKEN = "fc282b048adcc71a77bc00ace49e8a81" + + private var ERROR_TRACKER: ErrorTracker? = null + private var FAST_STATS_METRICS: BukkitContext? = null + + fun loadMetrics(plugin: CustomAnvil) { + if(DependencyManager.externGuiTester.isInTest()) return + + val config = ConfigHolder.DEFAULT_CONFIG.config + val metricString = config.getString(ConfigOptions.METRIC_TYPE, MetricType.AUTO.value)!! + val metricType = MetricType.from(metricString) + + val nmsType = DiagnosticExecutor.fetchNMSType() + val isAlpha = CustomAnvil.instance.description.version.contains("dev") + if(metricType.allowBStats) { + try { + val metric = Metrics(plugin, BSTATS_PLUGIN_ID) + metric.addCustomChart(Metrics.SimplePie("nms_type") { nmsType }) + metric.addCustomChart(Metrics.SimplePie("using_alpha") { isAlpha.toString() }) + } catch (_: Exception) {} + } + + if(metricType.allowFastStats) { + // Check support java 17 (metric only work in java 17) + val versionParts = System.getProperty("java.version").split(".") + val majorVersion = versionParts[0].toInt() + if (majorVersion >= 17) try { + faststatTelemetry(plugin, nmsType, isAlpha) + } catch (_: Throwable) {} + } + } + + private fun faststatTelemetry(plugin: CustomAnvil, nmsType: String, isAlpha: Boolean) { + val config = ConfigHolder.DEFAULT_CONFIG.config + val reportErrors = config.getBoolean(ConfigOptions.METRIC_COLLECT_ERROR, true) + if(reportErrors) + ERROR_TRACKER = ErrorTracker.contextAware() + + FAST_STATS_METRICS = BukkitContext.Factory(plugin, FASTSTATS_TOKEN) + .metrics { factory -> factory + .addMetric(Metric.string("nms_type") { nmsType }) + .addMetric(Metric.bool("replace_too_expensive") { ConfigOptions.doReplaceTooExpensive }) + .addMetric(Metric.bool("using_alpha") { isAlpha }) + .create() + } + .errorTrackerService(ERROR_TRACKER) + .create() + + if(reportErrors) FAST_STATS_METRICS!!.ready() + } + + fun shutdownMetrics() { + FAST_STATS_METRICS?.shutdown() + } + + var lastError: Throwable? = null + + fun trackError(e: Throwable) { + ERROR_TRACKER?.trackError(e) + lastError = e + } + + fun trackError(message: String) { + ERROR_TRACKER?.trackError(message) + } +} + +enum class MetricType( + val value: String, + val allowBStats: Boolean, + val allowFastStats: Boolean, +) { + AUTO("auto", true, true), + BSTATS("bstat", true, false), + FAST_STATS("faststats", false, true), + DISABLED("disabled", false, false), + ; + + companion object { + fun from(value: String): MetricType = entries.find { it.value == value } ?: AUTO + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MiniMessageUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MiniMessageUtil.kt new file mode 100644 index 0000000..c33cb9c --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MiniMessageUtil.kt @@ -0,0 +1,33 @@ +package xyz.alexcrea.cuanvil.util + +import net.kyori.adventure.text.TextComponent +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver +import net.kyori.adventure.text.minimessage.tag.standard.StandardTags +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil + +object MiniMessageUtil { + + val color_only_mm = MiniMessage.builder() + .tags( + TagResolver.resolver( + StandardTags.color(), + StandardTags.decorations() + ) + ) + .build() + + val mm = if (PlatformUtil.isPaper) MiniMessage.miniMessage() + else color_only_mm + + val legacy_mm = LegacyComponentSerializer.legacySection() + val plain_text_mm = PlainTextComponentSerializer.plainText() + + // Keeping track of this as most use of this can be replaced later on v2 with pure component alternative + fun fromLegacy(legacyText: String): TextComponent { + return legacy_mm.deserialize(legacyText) + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/NegativeSet.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/NegativeSet.kt new file mode 100644 index 0000000..a94175b --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/NegativeSet.kt @@ -0,0 +1,51 @@ +package xyz.alexcrea.cuanvil.util + +open class NegativeSet(val negate: MutableSet = HashSet()) : MutableSet { + + override fun iterator(): MutableIterator { + TODO("Not yet implemented") // can't be implemented I guess + } + + override fun add(element: T): Boolean { + return negate.remove(element) + } + + override fun remove(element: T): Boolean { + return negate.add(element) + } + + override fun addAll(elements: Collection): Boolean { + return negate.removeAll(elements.toSet()) + } + + override fun removeAll(elements: Collection): Boolean { + return negate.addAll(elements) + } + + override fun retainAll(elements: Collection): Boolean { + TODO("Not yet implemented") + } + + override fun clear() { + TODO("Not yet implemented") + } + + override fun isEmpty(): Boolean { + TODO("Not yet implemented") + } + + override val size: Int get() = TODO("Not yet implemented") + + override fun contains(element: T): Boolean { + return !negate.contains(element) + } + + override fun containsAll(elements: Collection): Boolean { + for (elm in elements) { + if(negate.contains(elm)) return false + } + + return true + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/UnitRepairUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/UnitRepairUtil.kt new file mode 100644 index 0000000..8463e27 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/UnitRepairUtil.kt @@ -0,0 +1,61 @@ +package xyz.alexcrea.cuanvil.util + +import org.bukkit.configuration.ConfigurationSection +import org.bukkit.inventory.ItemStack +import xyz.alexcrea.cuanvil.config.ConfigHolder +import xyz.alexcrea.cuanvil.util.MaterialUtil.customType + +object UnitRepairUtil { + + // Default value for user set default unit repair % + private const val DEFAULT_DEFAULT_UNIT_REPAIR = 0.25 + + // Path to user default unit repair value + private const val UNIT_REPAIR_DEFAULT_PATH = "default_repair_amount" + + /** + * Get the % of repair by unit [other] will do to this [ItemStack]. + * null if can't unit repaired by [other] + */ + fun ItemStack.getRepair( + other: ItemStack? + ): Double? { + if (other == null) return null + val config = ConfigHolder.UNIT_REPAIR_HOLDER.config + // Get configuration section if exist + val otherName = other.customType.key.lowercase() + var section = config.getConfigurationSection(otherName) + if (section == null) { + section = config.getConfigurationSection(otherName.uppercase()) + if (section == null) return null + + } + // Get repair amount + var userDefault = config.getDouble(UNIT_REPAIR_DEFAULT_PATH, DEFAULT_DEFAULT_UNIT_REPAIR) + if (userDefault <= 0) { + userDefault = DEFAULT_DEFAULT_UNIT_REPAIR + } + + return getRepairAmount(this, section, userDefault) + } + + /** + * Get the item % repaired by this configuration section of a unit repair. + * null if not found. + * If value is set to less than or equal to 0 then it will be set to default + */ + private fun getRepairAmount(item: ItemStack, section: ConfigurationSection, default: Double): Double? { + val itemName = item.customType.key.lowercase() + val repairValue = if (section.isDouble(itemName)) { + section.getDouble(itemName) + } else if (section.isDouble(itemName.uppercase())) { + section.getDouble(itemName.uppercase()) + } else { + return null + } + if (repairValue <= 0) + return default + return repairValue + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilColorUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilColorUtil.kt new file mode 100644 index 0000000..9564b2e --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilColorUtil.kt @@ -0,0 +1,306 @@ +package xyz.alexcrea.cuanvil.util.anvil + +import io.delilaheve.util.ConfigOptions +import net.kyori.adventure.text.Component +import org.bukkit.permissions.Permissible +import xyz.alexcrea.cuanvil.util.MiniMessageUtil +import java.util.regex.Matcher +import java.util.regex.Pattern + +object AnvilColorUtil { + private val HEX_PATTERN: Pattern = Pattern.compile("#[A-Fa-f0-9]{6}") // pattern to find hexadecimal string + private val TRANSFORMED_HEX_PATTERN = Pattern.compile("§x(§[0-9a-fA-F]){6}") // pattern to find minecraft hex string + + class ColorPermissions( + val canUseColorCode: Boolean, + val canUseHexColor: Boolean, + val canUseMinimessage: Boolean, + val permissible: Permissible, // source of the permission. tried to avoid needing it but meh + ) { + fun allowed(): Boolean { + return canUseColorCode || canUseHexColor || canUseMinimessage + } + + fun onlyMinimessage(): Boolean { + return canUseMinimessage && !canUseColorCode && !canUseHexColor + } + } + + fun calculatePermissions( + player: Permissible, + usePermission: Boolean, + allowColorCode: Boolean, + allowHexadecimalColor: Boolean, + allowMinimessage: Boolean, + useType: ColorUseType + ): ColorPermissions { + if (!allowColorCode && !allowHexadecimalColor && !allowMinimessage) + return ColorPermissions( + canUseColorCode = false, + canUseHexColor = false, + canUseMinimessage = false, + player, + ) + + val canUseColorCode = + allowColorCode && (!usePermission || useType.colorCodePerm == null || player.hasPermission( + useType.colorCodePerm + )) + + val canUseMinimessage = + allowMinimessage && (!usePermission || useType.minimessagePerm == null || player.hasPermission( + useType.minimessagePerm + )) + + val canUseHexColor = + allowHexadecimalColor && (!usePermission || useType.hexColorPerm == null || player.hasPermission( + useType.hexColorPerm + )) + + return ColorPermissions(canUseColorCode, canUseHexColor, canUseMinimessage, player) + } + + fun renamePermission(player: Permissible): ColorPermissions { + return calculatePermissions(player, + ConfigOptions.permissionNeededForColor, + ConfigOptions.allowColorCode, ConfigOptions.allowHexadecimalColor, ConfigOptions.allowMinimessage, + ColorUseType.RENAME) + } + + /** + * Color a string depending on permitted use + * @return colored component or null if nothing has been colored + */ + fun handleColor( + textToColorText: String, + permission: ColorPermissions, + + ): Component? { + if (!permission.allowed()) return null + + val textToColor = StringBuilder(textToColorText) + var useColor = false + // Handle color code + if (permission.canUseColorCode) { // maybe should use LegacyComponentSerializer ? + var nbReplacement = replaceAll(textToColor, "&", "§", 2) + nbReplacement -= 2 * replaceAll(textToColor, "§§", "&", 2) + + if (nbReplacement > 0) { + useColor = true + + if (ConfigOptions.usePerColorCodePermission) + filterPermissibleColorCode(textToColor, permission.permissible) + } + } + + if (permission.canUseHexColor) { + val nbReplacement = replaceHexToColor(textToColor, 7, permission.canUseMinimessage) + + if (nbReplacement > 0) useColor = true + } + + val previousStr = textToColor.toString() + var result: Component = MiniMessageUtil.legacy_mm.deserialize(previousStr) + if (permission.canUseMinimessage) { + // we dance with formats here + val toMinimessage = MiniMessageUtil.mm.serialize(result) + val hackySolution = toMinimessage.replace("\\<", "<") + val fromMinimessage = MiniMessageUtil.mm.deserialize(hackySolution) + val asPlain = MiniMessageUtil.plain_text_mm.serialize(fromMinimessage) + + if (previousStr != asPlain) { + useColor = true + result = fromMinimessage + } + } + + return if (useColor) result + else null + } + + private fun filterPermissibleColorCode(textToColor: StringBuilder, player: Permissible) { + var index = 0 + while (true) { + index = textToColor.indexOf('§', index) + if (index == -1 || index == textToColor.length - 1) return + + val next = textToColor[index + 1] + // check permission for this color + if(!player.hasPermission("ca.color.code.$next")) + textToColor.replace(index, index + 1, "&") + + index++ + } + } + + /** + * Best effort to revert a component to the smallest allowed string + * that would result in it getting closest as possible to handleColor + * with current set of permitted use + * @return a new component if had any change. null otherwise + */ + fun revertColorSmallest( + component: Component?, + permission: ColorPermissions + ): String? { + if (!permission.allowed() || component == null) return null + + val transformed = MiniMessageUtil.mm.serialize(component) + val plainTransform = MiniMessageUtil.plain_text_mm.serialize(component) + if (transformed == plainTransform) return null + if (permission.onlyMinimessage()) { + return transformed + } + + // smol dance so we transform the component that may contain other tag into only decoration & color for legacy + val coloredMessage = MiniMessageUtil.color_only_mm.deserialize(transformed) + val legacyMessage = StringBuilder(MiniMessageUtil.legacy_mm.serialize(coloredMessage)) + + // Reverse hex pattern + if (permission.canUseHexColor) { + replaceColorToHex(legacyMessage, 14) + } + + // Reverse color pattern + if (permission.canUseColorCode) { + replaceAll(legacyMessage, "&", "&&", 1) + replaceAll(legacyMessage, "§", "&", 2) + } + + // In case we still has some § around by lack of permission we need to convert it back from legacy + // In other word it's time for dance #3 + val fromLegacy = MiniMessageUtil.legacy_mm.deserialize(legacyMessage.toString()) + val middleGround = MiniMessageUtil.mm.serialize(fromLegacy) + val hackySolutionStb = StringBuilder(middleGround) + replaceAll(hackySolutionStb, "\\<", "<", 2) + val hackySolution = hackySolutionStb.toString() + + val result: String = + if (permission.canUseMinimessage) hackySolution + else MiniMessageUtil.mm.stripTags(hackySolution) + + return if (result == plainTransform) null + else result + } + + /** + * Replace every instance of "from" to "to". + * @param builder The builder to replace the string from. + * @param from The source that should be replaced. + * @param to The string that should replace. + * @param endOffset Amount of character that should be ignored at the end. + * @return The number of replacement was that was done. + */ + private fun replaceAll(builder: java.lang.StringBuilder, from: String, to: String, endOffset: Int): Int { + var index = builder.indexOf(from) + var numberOfChanges = 0 + + while (index != -1 && index < builder.length - endOffset) { + builder.replace(index, index + from.length, to) + index += to.length + index = builder.indexOf(from, index) + + numberOfChanges += 1 + } + + return numberOfChanges + } + + /** + * Replace every hex color formatted like #000000 to the minecraft format + * @param builder The builder to replace the hex color from. + * @param endOffset Amount of character that should be ignored at the end. + * @return The number of replacement was that was done. + */ + private fun replaceHexToColor(builder: StringBuilder, endOffset: Int, checkTag: Boolean): Int { + val matcher: Matcher = HEX_PATTERN.matcher(builder) + + var numberOfChanges = 0 + var startIndex = 0 + + while (matcher.find(startIndex)) { + startIndex = matcher.start() + if (startIndex >= builder.length - endOffset) break //HOW AND WHERE WOULD THIS HAPPEN ????? + if (checkTag && isInTag(builder, startIndex)) { + startIndex += 1 // Avoid infinite loop + continue + } + + builder.replace(startIndex, startIndex + 1, "§x") + startIndex += 2 + for (i in 0..5) { + builder.insert(startIndex, '§') + startIndex += 2 + } + + numberOfChanges += 1 + } + + return numberOfChanges + } + + // Simple check if < > with some smart check like <> > not taken into account + // This is easily bypassable but if the player want to bypass he has better alternative + // AKA should avoid getting into any tag + private fun isInTag(builder: StringBuilder, index: Int): Boolean { + // Check left tag we have < after last > + val left = builder.slice(0..index) + val leftIndex = left.lastIndexOf("<") + var rightIndex = left.lastIndexOf(">") + + // last < do not exist or is before last > + if (leftIndex == -1 || rightIndex > leftIndex) return false + + val right = builder.slice(index..") + + // first > do not exist or is after first < (if exist) + if (rightIndex == -1 || (newleftIndex != -1 && newleftIndex < rightIndex)) return false + + // Then finally we use minimessage to check for tag + val expectedTag = builder.substring(leftIndex, newleftIndex + index + 1) + val notag = MiniMessageUtil.mm.stripTags(expectedTag) + + return notag != expectedTag + } + + /** + * Replace every hex color from the minecraft format to a format like #000000 + * @param builder The builder to replace the minecraft hex color from. + * @param endOffset Amount of character that should be ignored at the end. + * @return The number of replacement was that was done. + */ + private fun replaceColorToHex(builder: StringBuilder, endOffset: Int): Int { + val matcher: Matcher = TRANSFORMED_HEX_PATTERN.matcher(builder) + + var numberOfChanges = 0 + var startIndex = 0 + + while (matcher.find(startIndex)) { + startIndex = matcher.start() + if (startIndex >= builder.length - endOffset) break //HOW AND WHERE WOULD THIS HAPPEN ????? + + builder.replace(startIndex, startIndex + 2, "#") + startIndex += 1 + for (i in 0..5) { + builder.deleteCharAt(startIndex) + startIndex += 1 + } + + numberOfChanges += 1 + } + + return numberOfChanges + } + + enum class ColorUseType( + val colorCodePerm: String?, + val hexColorPerm: String?, + val minimessagePerm: String? + ) { + RENAME("ca.color.code", "ca.color.hex", "ca.rename.minimessage"), + LORE_EDIT(null, null, null) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilLoreEditUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilLoreEditUtil.kt new file mode 100644 index 0000000..a021b46 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilLoreEditUtil.kt @@ -0,0 +1,368 @@ +package xyz.alexcrea.cuanvil.util.anvil + +import net.kyori.adventure.text.Component +import org.bukkit.entity.HumanEntity +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.BookMeta +import org.bukkit.permissions.Permissible +import xyz.alexcrea.cuanvil.anvil.AnvilCost +import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.LoreEditResult +import xyz.alexcrea.cuanvil.dependency.DependencyManager +import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil.componentLore +import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil.setComponentLore +import xyz.alexcrea.cuanvil.util.MiniMessageUtil +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil +import xyz.alexcrea.cuanvil.util.config.LoreEditType +import java.util.* +import java.util.concurrent.atomic.AtomicReference + +object AnvilLoreEditUtil { + + private const val LORE_BY_BOOK: String = "ca.lore_edit.book" + private const val LORE_BY_PAPER: String = "ca.lore_edit.paper" + + private fun hasLoreEditByBookPermission(player: Permissible): Boolean { + return !LoreEditConfigUtil.bookLoreEditNeedPermission || player.hasPermission(LORE_BY_BOOK) + } + + private fun hasLoreEditByPaperPermission(player: Permissible): Boolean { + return !LoreEditConfigUtil.paperLoreEditNeedPermission || player.hasPermission(LORE_BY_PAPER) + } + + fun handleLoreAppendByBook( + player: Permissible, + first: ItemStack, + book: BookMeta, + cost: AnvilCost + ): ItemStack? { + if (!hasLoreEditByBookPermission(player)) return null + + val result = first.clone() + val meta = result.itemMeta ?: return null + val lore = meta.componentLore() + + val page = book.pages[0] + val lines = ArrayList(page.split("\n")) + val outLines = ArrayList(lines.size) + val colorCost = colorLines( + player, LoreEditType.APPEND_BOOK, + lines, outLines + ) + + lore.addAll(outLines) + + meta.setComponentLore(lore) + result.itemMeta = meta + + if (result == first) return null + + // Handle xp + cost.lore = colorCost // Cost of using color + cost.lore += outLines.size * LoreEditType.APPEND_BOOK.perLineCost // per line cost + baseEditLoreXpCost(cost, first, result, LoreEditType.APPEND_BOOK) // Fixed cost and work penalty + + return result + } + + fun handleLoreRemoveByBook(player: Permissible, first: ItemStack, cost: AnvilCost): ItemStack? { + if (!hasLoreEditByBookPermission(player)) return null + + // remove lore + val result = first.clone() + val leftMeta = result.itemMeta ?: return null + val currentLore = DependencyManager.stripLore(result) + if (currentLore.isEmpty()) return null + + val uncolorCost = uncolorLines(player, currentLore, LoreEditType.REMOVE_BOOK) + + leftMeta.lore = null + result.itemMeta = leftMeta + + DependencyManager.updateLore(result) + if (result == first) return null + + // Handle xp + cost.lore = uncolorCost + cost.lore += currentLore.size * LoreEditType.REMOVE_BOOK.perLineCost + baseEditLoreXpCost(cost, first, result, LoreEditType.REMOVE_BOOK) + + return result + } + + // Return true if appended, false if removed, null if neither + fun bookLoreEditIsAppend(first: ItemStack, second: ItemStack): Boolean? { + // Test if the book & quil contain content + val meta = second.itemMeta as BookMeta? ?: return false + + var hasContent = false + if (meta.hasPages() && meta.pageCount >= 1) { + // Test if the pages is ok + for (page in meta.pages) { + if (page.isNotBlank()) { + hasContent = true + break + } + } + } + + // We don't want to "add" the first page is there is content and the first page is empty + if (hasContent) { + if (meta.pages[0].isEmpty()) return null + if (LoreEditType.APPEND_BOOK.enabled) + return true + } else if (LoreEditType.REMOVE_BOOK.enabled) { + if (!first.hasItemMeta()) return null + + val leftMeta = first.itemMeta!! + return if (leftMeta.hasLore()) false + else null + } + return null + } + + fun tryLoreEditByBook(player: HumanEntity, first: ItemStack, second: ItemStack): LoreEditResult { + val isAppend = bookLoreEditIsAppend(first, second) ?: return LoreEditResult.EMPTY + val type = if (isAppend) LoreEditType.APPEND_BOOK else LoreEditType.REMOVE_BOOK + + val meta = second.itemMeta as BookMeta + val cost = AnvilCost() + val item = if (isAppend) + handleLoreAppendByBook(player, first, meta, cost) + else handleLoreRemoveByBook(player, first, cost) + + return LoreEditResult(item, cost, type) + } + + // Return true if appended, false if removed, null if neither + fun paperLoreEditIsAppend(first: ItemStack, second: ItemStack): Boolean? { + // Test if the paper contain a display name + val meta = second.itemMeta ?: return false + + val hasContent = meta.hasDisplayName() + if (hasContent) { + if (LoreEditType.APPEND_PAPER.enabled) + return true + } else if (LoreEditType.REMOVE_PAPER.enabled) { + if (!first.hasItemMeta()) return null + + val leftMeta = first.itemMeta!! + return if (leftMeta.hasLore() && leftMeta.lore!!.isNotEmpty()) false + else null + } + return null + } + + fun handleLoreAppendByPaper( + player: Permissible, + first: ItemStack, + second: ItemStack, + cost: AnvilCost + ): ItemStack? { + if (!hasLoreEditByPaperPermission(player)) return null + + val result = first.clone() + val meta = result.itemMeta ?: return null + val lore = meta.componentLore() + + val appendEnd = LoreEditConfigUtil.paperLoreOrderIsEnd + + // A bit overdone to color 1 line but hey + val outList = ArrayList(1) + val colorCost = colorLines( + player, LoreEditType.APPEND_PAPER, + Collections.singletonList(second.itemMeta!!.displayName), + outList + ) + + val line = outList[0] + if (appendEnd) + lore.add(line) + else + lore.add(0, line) + + meta.setComponentLore(lore) + result.itemMeta = meta + + if (result == first) return null + + // Handle xp + cost.lore = colorCost + baseEditLoreXpCost(cost, first, result, LoreEditType.APPEND_PAPER) + + return result + } + + fun handleLoreRemoveByPaper(player: Permissible, first: ItemStack, cost: AnvilCost): ItemStack? { + if (!hasLoreEditByPaperPermission(player)) return null + + // remove lore line + val result = first.clone() + val meta = result.itemMeta!! + + val removeEnd = LoreEditConfigUtil.paperLoreOrderIsEnd + val lore = DependencyManager.stripLore(result) + if (lore.isEmpty()) return null + + val line = if (removeEnd) lore.removeAt(lore.size - 1) + else lore.removeAt(0) + + meta.lore = null + result.itemMeta = meta + + // Update lore but make sure custom lore is put last + DependencyManager.updateLore(result) + + val finalLore = ArrayList() + finalLore.addAll(meta.componentLore()) + finalLore.addAll(lore) + + meta.setComponentLore(finalLore) + result.itemMeta = meta + if (result == first) return null + + // Get color cost to uncolor this line + val uncolorCost = uncolorLine(player, line, LoreEditType.REMOVE_PAPER) + + // Handle other xp + cost.lore = uncolorCost + baseEditLoreXpCost(cost, first, result, LoreEditType.REMOVE_PAPER) + + return result + } + + fun tryLoreEditByPaper( + player: HumanEntity, + first: ItemStack, + second: ItemStack + ): LoreEditResult { + val isAppend = paperLoreEditIsAppend(first, second) ?: return LoreEditResult.EMPTY + val type = if (isAppend) LoreEditType.APPEND_BOOK else LoreEditType.REMOVE_BOOK + + val cost = AnvilCost() + val item = if (isAppend) + handleLoreAppendByPaper(player, first, second, cost) + else handleLoreRemoveByPaper(player, first, cost) + + return LoreEditResult(item, cost, type) + } + + private fun baseEditLoreXpCost( + cost: AnvilCost, + first: ItemStack, + result: ItemStack, + editType: LoreEditType + ) { + cost.lore += editType.fixedCost + + cost.workPenalty = AnvilXpUtil.calculatePenalty(first, null, result, editType.useType) + } + + fun colorPermission(player: Permissible, editType: LoreEditType): AnvilColorUtil.ColorPermissions { + return AnvilColorUtil.calculatePermissions( + player, + false, + editType.allowColorCode, + editType.allowHexColor, + editType.allowMinimessage, + AnvilColorUtil.ColorUseType.LORE_EDIT + ) + } + + private fun colorLine(line: String, permission: AnvilColorUtil.ColorPermissions): Component? { + return AnvilColorUtil.handleColor( + line, + permission + ) + } + + private fun colorLines( + player: Permissible, editType: LoreEditType, + lines: List, outLines: MutableList + ): Int { + val permission = colorPermission(player, editType) + val colorCost = editType.useColorCost + + // Handle color and minimessage of each lines + var hasUsedColor = false + for (line in lines) { + val component = colorLine(line, permission) + + if (component != null) { + hasUsedColor = true + outLines.add(component) + } else { + outLines.add(Component.text(line)) + } + } + + return if (hasUsedColor) colorCost + else 0 + } + + fun uncolorLines(player: Permissible, lines: MutableList, editType: LoreEditType): Int { + val permission = colorPermission(player, editType) + + // Now handle color of each lines + var hasUndidColor = false + for ((index, line) in lines.withIndex()) { + if (line == null) { + lines[index] = null + continue + } + + val clearedLine = AnvilColorUtil.revertColorSmallest( + line, + permission + ) + + val result: String + if (clearedLine != null) { + hasUndidColor = true + result = clearedLine + } else { + result = MiniMessageUtil.plain_text_mm.serialize(line) + } + + lines[index] = MiniMessageUtil.plain_text_mm.deserialize(result) + } + + return if (hasUndidColor) { + editType.removeColorCost + } else { + 0 + } + } + + // do not output the uncolored line... + fun uncolorLine(player: Permissible, line: Component?, editType: LoreEditType): Int { + return uncolorLine(player, AtomicReference(line), editType) + } + + fun uncolorLine(player: Permissible, line: AtomicReference, editType: LoreEditType): Int { + val coloredComponent = line.get() ?: return 0 + val permission = colorPermission(player, editType) + + val clearedLine = AnvilColorUtil.revertColorSmallest( + coloredComponent, + permission + ) + + var hasUndidColor = false + val result: String + if (clearedLine != null) { + hasUndidColor = true + result = clearedLine + } else { + // Remove extra tags + result = MiniMessageUtil.plain_text_mm.serialize(coloredComponent) + } + line.set(MiniMessageUtil.plain_text_mm.deserialize(result)) + + return if (hasUndidColor) { + editType.removeColorCost + } else { + 0 + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilUseTypeUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilUseTypeUtil.kt new file mode 100644 index 0000000..a9c7f39 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilUseTypeUtil.kt @@ -0,0 +1,26 @@ +package xyz.alexcrea.cuanvil.util.anvil + +import io.delilaheve.util.ConfigOptions + +object AnvilUseTypeUtil { + + /* + By stupidity of kotlin not allowing static function outside of companion object I need to do another util class only for 1 function: + Companion object on enum class seems to get initialized AFTER the enum itself + that mean. if you want to call a static function on the enum class itself YOU CAN'T. you need to do this stupid thing + or you can make a stupid top level function OR object that even worse because of global scope pollution + (btw was gpt ""solution"". fortunately I do not "vibe code" so I do what I know instead of stupid AI solution & code) + I mean, this is still global scope pollution bc of a USELESS class that SHOULD not exist but as a class is better than a random *ss function + + sorry for the rent but this made me frustrated + + Note: I still like a lot of part of kotlin compared to java but this part is one that I hate + */ + /** + * Get config path for normal anvil use + */ + fun defaultPath(typeName: String): String { + return "${ConfigOptions.WORK_PENALTY_ROOT}.$typeName" + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt new file mode 100644 index 0000000..4846f31 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt @@ -0,0 +1,308 @@ +package xyz.alexcrea.cuanvil.util.anvil + +import io.delilaheve.CustomAnvil +import io.delilaheve.util.ConfigOptions +import io.delilaheve.util.EnchantmentUtil.enchantmentName +import io.delilaheve.util.ItemUtil.findEnchantments +import io.delilaheve.util.ItemUtil.isEnchantedBook +import org.bukkit.GameMode +import org.bukkit.NamespacedKey +import org.bukkit.entity.HumanEntity +import org.bukkit.entity.Player +import org.bukkit.inventory.AnvilInventory +import org.bukkit.inventory.InventoryView +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.Repairable +import org.bukkit.persistence.PersistentDataType +import xyz.alexcrea.cuanvil.anvil.AnvilCost +import xyz.alexcrea.cuanvil.anvil.AnvilUseType +import xyz.alexcrea.cuanvil.config.ConfigHolder +import xyz.alexcrea.cuanvil.dependency.DependencyManager +import xyz.alexcrea.cuanvil.dependency.economy.EconomyManager +import xyz.alexcrea.cuanvil.group.ConflictType +import xyz.alexcrea.cuanvil.util.AnvilTitleUtil +import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil + +object AnvilXpUtil { + + const val EXCLUSIVE_PENALTY_PREFIX = "repair_cost" + + /** + * Display the required cost (either as xp or as ) + */ + fun setAnvilInvCost( + inventory: AnvilInventory, + view: InventoryView, + player: Player, + cost: AnvilCost, + ignoreRules: Boolean = false + ) { + if (ConfigOptions.shouldUseMoney(player)) { + cost.isMonetary = true + setAnvilPrice(inventory, view, player, cost) + } else + setAnvilInvXp(inventory, view, player, cost.filteredXpCost(ignoreRules), ignoreRules) + } + + fun maximumXpCost(ignoreRules: Boolean = false): Int { + return if (ConfigOptions.doRemoveCostLimit || ignoreRules) { + Int.MAX_VALUE + } else { + ConfigOptions.maxAnvilCost + 1 + } + } + + /** + * Display xp needed for the work on the anvil inventory + */ + private fun setAnvilInvXp( + inventory: AnvilInventory, + view: InventoryView, + player: HumanEntity, + anvilCost: Int, + ignoreRules: Boolean = false + ) { + val maximumRepairCost = maximumXpCost(ignoreRules) + + // Try first just in case another plugin, or the test need this + inventory.maximumRepairCost = maximumRepairCost + inventory.repairCost = anvilCost + // TODO for 2.x.x use anvil view & set directly there + + /* Because Minecraft likes to have the final say in the repair cost displayed + * we need to wait for the event to end before overriding it, this ensures that + * we have the final say in the process. */ + DependencyManager.scheduler.scheduleOnEntity( + CustomAnvil.instance, player + ) { + // retry after a tick + inventory.maximumRepairCost = maximumRepairCost + inventory.repairCost = anvilCost + // TODO for 2.x.x use anvil view & set directly there + + if (player !is Player) return@scheduleOnEntity + + if (player.gameMode != GameMode.CREATIVE) { + val bypassToExpensive = (ConfigOptions.doReplaceTooExpensive) && + (anvilCost >= 40) && + anvilCost < inventory.maximumRepairCost + + DependencyManager.packetManager.setInstantBuild(player, bypassToExpensive) + } + + player.updateInventory() + } + } + + /** + * Display monetary cost needed for the work on the anvil inventory + */ + private fun setAnvilPrice( + inventory: AnvilInventory, + view: InventoryView, + player: Player, + cost: AnvilCost, + ) { + val finalCost = cost.asMonetaryCost() + + val has = player.gameMode == GameMode.CREATIVE || + EconomyManager.economy!!.has(player, finalCost) + + val text = "Cost: " + (if (has) "§2" else "§4") + + EconomyManager.economy!!.format(finalCost) + AnvilTitleUtil.rename( + view, text, + player, + AnvilRenameDialogUtil.anvilRenameDialog, + CustomAnvil.instance + ) + + clearAnvilXpCost(inventory, view, player) + } + + private fun clearAnvilXpCost( + inventory: AnvilInventory, + view: InventoryView, + player: HumanEntity, + ) { + // TODO for 2.x.x use anvil view & set directly there + inventory.repairCost = 0 + + // retry after a tick + DependencyManager.scheduler.scheduleOnEntity( + CustomAnvil.instance, player + ) { + inventory.repairCost = 0 + + if (player !is Player) return@scheduleOnEntity + player.updateInventory() + } + } + + /** + * Function to calculate work penalty of anvil work + * Also change result work penalty if right item is not null + */ + fun calculatePenalty(left: ItemStack, right: ItemStack?, result: ItemStack, useType: AnvilUseType): Int { + // Extracted From https://minecraft.wiki/w/Anvil_mechanics#Enchantment_equation + // Calculate work penalty + val penaltyType = ConfigOptions.workPenaltyPart(useType) + val leftPenalty = (left.itemMeta as? Repairable)?.repairCost ?: 0 + val leftExclusivePenalty = findExclusivePenalty(left, useType) + + val rightPenalty = + if (right == null) 0 + else (right.itemMeta as? Repairable)?.repairCost ?: 0 + val rightExclusivePenalty = findExclusivePenalty(right, useType) + + // Increase penalty on fusing or unit repair + if (penaltyType.penaltyIncrease) { + result.itemMeta?.let { + (it as? Repairable)?.repairCost = leftPenalty.coerceAtLeast(rightPenalty) * 2 + 1 + result.itemMeta = it + } + } + if (penaltyType.exclusivePenaltyIncrease) { + val resultPenalty = leftExclusivePenalty.coerceAtLeast(rightExclusivePenalty) * 2 + 1 + setExclusivePenalty(result, resultPenalty, useType) + } + + CustomAnvil.log( + "Calculated penalty: " + + "leftPenalty: $leftPenalty, " + + "rightPenalty: $rightPenalty, " + + "result penalty: ${(result.itemMeta as? Repairable)?.repairCost ?: "none"}" + ) + + var resultSum = 0 + if (penaltyType.penaltyAdditive) { + resultSum += leftPenalty + rightPenalty + } + if (penaltyType.exclusivePenaltyAdditive) { + resultSum += leftExclusivePenalty + rightExclusivePenalty + } + + return resultSum + } + + fun onNoResult(player: HumanEntity, view: InventoryView) { + if (ConfigOptions.shouldUseMoney(player)) + AnvilTitleUtil.rename( + view, "Repair & Name", + player, + AnvilRenameDialogUtil.anvilRenameDialog, + CustomAnvil.instance + ) + } + + private fun exclusivePenaltyKey(useType: AnvilUseType): NamespacedKey { + return NamespacedKey(CustomAnvil.instance, "${EXCLUSIVE_PENALTY_PREFIX}_${useType.typeName}") + } + + private fun setExclusivePenalty( + result: ItemStack, + resultPenalty: Int, + useType: AnvilUseType + ) { + val key = exclusivePenaltyKey(useType) + + val meta = result.itemMeta!! + meta.persistentDataContainer.set(key, PersistentDataType.INTEGER, resultPenalty) + result.itemMeta = meta + } + + private fun findExclusivePenalty( + item: ItemStack?, + useType: AnvilUseType + ): Int { + if (item == null || !item.hasItemMeta()) return 0 + val key = exclusivePenaltyKey(useType) + + val meta = item.itemMeta!! + return meta.persistentDataContainer.get(key, PersistentDataType.INTEGER) ?: return 0 + } + + /** + * Function to calculate right enchantment values + * it include enchantment placed on final item and conflicting enchantment + */ + fun getRightValues(right: ItemStack, result: ItemStack, cost: AnvilCost) { + // Calculate right value and illegal enchant penalty + + val rightIsFormBook = right.isEnchantedBook() + val resultEnchs = result.findEnchantments() + val resultEnchsKeys = HashMap(resultEnchs) + + for (enchantment in right.findEnchantments()) { + // count enchant as illegal enchant if it conflicts with another enchant or not in result + if ((enchantment.key !in resultEnchsKeys)) { + resultEnchsKeys[enchantment.key] = enchantment.value + val conflictType = ConfigHolder.CONFLICT_HOLDER.conflictManager.isConflicting( + resultEnchsKeys, + result, + enchantment.key + ) + resultEnchsKeys.remove(enchantment.key) + + if (ConflictType.ENCHANTMENT_CONFLICT == conflictType) { + cost.illegalPenalty += ConfigOptions.sacrificeIllegalCost + CustomAnvil.verboseLog("Big conflict. Adding illegal price penalty") + } + continue + } + // We know "enchantment.key in resultEnchs" true + val resultLevel = resultEnchs[enchantment.key]!! + + val enchantmentMultiplier = ConfigOptions.enchantmentValue(enchantment.key, rightIsFormBook) + val value = resultLevel * enchantmentMultiplier + CustomAnvil.log("Value for ${enchantment.key.enchantmentName} level ${enchantment.value} is $value ($resultLevel * $enchantmentMultiplier)") + cost.enchantment += value + + } + CustomAnvil.log( + "Calculated right values: " + + "rightValue: ${cost.enchantment}, " + + "illegalPenalty: ${cost.illegalPenalty}" + ) + } + + /** + * Calculate the maximum level reachable with this amount of `xp` + * This is equivalent of the displayed level on client + * @author provided by kFor + */ + fun calculateLevelForXp(xp: Int): Int { + return when { + xp <= 352 -> (Math.sqrt((xp + 9).toDouble()) - 3).toInt() + xp <= 1507 -> { + val inner = (2.0 / 5.0) * (xp - 7839.0 / 40.0) + (81.0 / 10.0 + Math.sqrt(inner)).toInt() + } + + else -> { + val inner = (2.0 / 9.0) * (xp - 54215.0 / 72.0) + (325.0 / 18.0 + Math.sqrt(inner)).toInt() + } + } + } + + /** + * Calculate the minimum level necessary to have at least `xp` + */ + fun calculateMinimumLevelForXp(xp: Int): Int { + return calculateLevelForXp(xp - 1) + 1 + } + + /** + * Calculate the minimum amount of xp necessary to reach `level` + * @author provided by kFor + */ + fun calculateXpForLevel(level: Int): Int { + return when { + level <= 16 -> (level * level + 6 * level) + level <= 31 -> (2.5 * level * level - 40.5 * level + 360).toInt() + else -> (4.5 * level * level - 162.5 * level + 2220).toInt() + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/config/LoreEditConfigUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/config/LoreEditConfigUtil.kt new file mode 100644 index 0000000..9d0eb6a --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/config/LoreEditConfigUtil.kt @@ -0,0 +1,103 @@ +package xyz.alexcrea.cuanvil.util.config + +import xyz.alexcrea.cuanvil.config.ConfigHolder.DEFAULT_CONFIG as CONFIG + +object LoreEditConfigUtil { + + // Per edit type configs Path + const val IS_ENABLED = "enabled" + const val FIXED_COST = "fixed_cost" + const val PER_LINE_COST = "per_line_cost" + const val DO_CONSUME = "do_consume" + + // Permission configs path + const val BOOK_PERMISSION_NEEDED = "lore_edit.book_and_quil.use_permission" + const val PAPER_PERMISSION_NEEDED = "lore_edit.paper.use_permission" + + // Color configs path + const val ALLOW_COLOR_CODE = "allow_color_code" + const val ALLOW_HEX_COLOR = "allow_hexadecimal_color" + const val ALLOW_MINIMESSAGE = "allow_minimessage" + const val USE_COLOR_COST = "use_cost" + + const val REMOVE_COLOR_COST = "remove_color_cost" + + // Lore order config path + const val PAPER_EDIT_ORDER = "lore_edit.paper.order" + + // -------------- + // Default Values + // -------------- + + // Per edit type configs defaults + const val DEFAULT_IS_ENABLED = false + const val DEFAULT_FIXED_COST = 1 + const val DEFAULT_PER_LINE_COST = 0 + const val DEFAULT_DO_CONSUME = false + + // Permission configs defaults + const val DEFAULT_BOOK_PERMISSION_NEEDED = true + const val DEFAULT_PAPER_PERMISSION_NEEDED = true + + // Color configs defaults + const val DEFAULT_ALLOW_COLOR_CODE = true + const val DEFAULT_ALLOW_HEX_COLOR = true + const val DEFAULT_ALLOW_MINIMESSAGE = true + const val DEFAULT_USE_COLOR_COST = 0 + + const val DEFAULT_REMOVE_COLOR_COST = 0 + + // Lore order config default + const val DEFAULT_PAPER_EDIT_ORDER = "end" + + // ------------- + // Config Ranges + // ------------- + + val FIXED_COST_RANGE = 0..1000 + val PER_LINE_COST_RANGE = 0..1000 + + val USE_COLOR_COST_RANGE = 0..1000 + val REMOVE_COLOR_COST_RANGE = 0..1000 + + // ------------------- + // Generic Get methods + // ------------------- + + /** + * Get if we should append/remove at the end or at the start of the lore list + * This may change to an "OrderType" enum or equivalent later + */ + val paperLoreOrderIsEnd: Boolean + get() { + return CONFIG + .config + .getString(PAPER_EDIT_ORDER, DEFAULT_PAPER_EDIT_ORDER) + .equals(DEFAULT_PAPER_EDIT_ORDER, ignoreCase = true) + } + + /** + * If lore edit via book need permission + */ + val bookLoreEditNeedPermission: Boolean + get() { + return CONFIG + .config + .getBoolean(BOOK_PERMISSION_NEEDED, DEFAULT_BOOK_PERMISSION_NEEDED) + } + + /** + * If lore edit via paper need permission + */ + val paperLoreEditNeedPermission: Boolean + get() { + return CONFIG + .config + .getBoolean(PAPER_PERMISSION_NEEDED, DEFAULT_PAPER_PERMISSION_NEEDED) + } + + // ----------------- + // Color Get methods + // ----------------- + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/config/LoreEditType.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/config/LoreEditType.kt new file mode 100644 index 0000000..c094c71 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/config/LoreEditType.kt @@ -0,0 +1,142 @@ +package xyz.alexcrea.cuanvil.util.config + +import xyz.alexcrea.cuanvil.anvil.AnvilUseType +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.ALLOW_COLOR_CODE +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.ALLOW_HEX_COLOR +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.ALLOW_MINIMESSAGE +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.DEFAULT_ALLOW_COLOR_CODE +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.DEFAULT_ALLOW_HEX_COLOR +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.DEFAULT_ALLOW_MINIMESSAGE +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.DEFAULT_REMOVE_COLOR_COST +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.DEFAULT_USE_COLOR_COST +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.REMOVE_COLOR_COST +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.REMOVE_COLOR_COST_RANGE +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.USE_COLOR_COST +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.USE_COLOR_COST_RANGE +import xyz.alexcrea.cuanvil.config.ConfigHolder.DEFAULT_CONFIG as CONFIG + +enum class LoreEditType( + val rootPath: String, + val useType: AnvilUseType, + val isBook: Boolean, + val isAppend: Boolean, + val isMultiLine: Boolean, +) { + APPEND_BOOK(AnvilUseType.LORE_EDIT_BOOK_APPEND, true, true, true), + REMOVE_BOOK(AnvilUseType.LORE_EDIT_BOOK_REMOVE, true, false, true), + APPEND_PAPER(AnvilUseType.LORE_EDIT_PAPER_APPEND, false, true, false), + REMOVE_PAPER(AnvilUseType.LORE_EDIT_PAPER_REMOVE, false, false, false), + ; + + constructor( + useType: AnvilUseType, + isPaper: Boolean, + isAppend: Boolean, + isMultiLine: Boolean, + ) : this(useType.path, useType, + isPaper, isAppend, isMultiLine) + + /** + * If this edit type is enabled + */ + val enabled: Boolean + get() { + return CONFIG + .config + .getBoolean("${rootPath}.${LoreEditConfigUtil.IS_ENABLED}", LoreEditConfigUtil.DEFAULT_IS_ENABLED) + } + + /** + * Fixed cost added to this edit + */ + val fixedCost: Int + get() { + return CONFIG + .config + .getInt("${rootPath}.${LoreEditConfigUtil.FIXED_COST}", LoreEditConfigUtil.DEFAULT_FIXED_COST) + .takeIf { it in LoreEditConfigUtil.FIXED_COST_RANGE } + ?: LoreEditConfigUtil.DEFAULT_FIXED_COST + } + + /** + * Cost added per line added + */ + val perLineCost: Int + get() { + if (!isMultiLine) throw IllegalStateException("Per line cost get on single line edit type") + return CONFIG + .config + .getInt("${rootPath}.${LoreEditConfigUtil.PER_LINE_COST}", LoreEditConfigUtil.DEFAULT_PER_LINE_COST) + .takeIf { it in LoreEditConfigUtil.PER_LINE_COST_RANGE } + ?: LoreEditConfigUtil.DEFAULT_PER_LINE_COST + } + + /** + * If the edit should consume the provided material + */ + val doConsume: Boolean + get() { + return CONFIG + .config + .getBoolean("${rootPath}.${LoreEditConfigUtil.DO_CONSUME}", LoreEditConfigUtil.DEFAULT_DO_CONSUME) + } + + /** + * Allow usage or removal of color code + */ + val allowColorCode: Boolean + get() { + return CONFIG + .config + .getBoolean("$rootPath.$ALLOW_COLOR_CODE", DEFAULT_ALLOW_COLOR_CODE) + } + + /** + * Allow usage or removal of hexadecimal color + */ + val allowHexColor: Boolean + get() { + return CONFIG + .config + .getBoolean("${rootPath}.$ALLOW_HEX_COLOR", DEFAULT_ALLOW_HEX_COLOR) + } + + /** + * Allow usage or removal of minimessage on lore add + */ + val allowMinimessage: Boolean + get() { + return CONFIG + .config + .getBoolean("${rootPath}.$ALLOW_MINIMESSAGE", DEFAULT_ALLOW_MINIMESSAGE) + } + + /** + * Cost when using either color code and hex color on lore add + */ + val useColorCost: Int + get() { + if (!isAppend) throw IllegalStateException("Can only call with an append edit type") + return CONFIG + .config + .getInt("${rootPath}.$USE_COLOR_COST", DEFAULT_USE_COLOR_COST) + .takeIf { it in USE_COLOR_COST_RANGE } + ?: DEFAULT_USE_COLOR_COST + + } + + /** + * Cost when using either color code and hex color on lore remove + */ + val removeColorCost: Int + get() { + if (isAppend) throw IllegalStateException("Can only call with a remove edit type") + return CONFIG + .config + .getInt("${rootPath}.$REMOVE_COLOR_COST", DEFAULT_REMOVE_COLOR_COST) + .takeIf { it in REMOVE_COLOR_COST_RANGE } + ?: DEFAULT_REMOVE_COLOR_COST + + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/dialog/AnvilRenameDialogUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/dialog/AnvilRenameDialogUtil.kt new file mode 100644 index 0000000..07d4e08 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/dialog/AnvilRenameDialogUtil.kt @@ -0,0 +1,55 @@ +package xyz.alexcrea.cuanvil.util.dialog + +import io.delilaheve.CustomAnvil +import io.delilaheve.util.ConfigOptions +import org.bukkit.entity.HumanEntity +import org.bukkit.event.inventory.PrepareAnvilEvent +import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil +import xyz.alexcrea.cuanvil.dialog.AnvilRenameDialog +import xyz.alexcrea.cuanvil.dialog.AnvilRenameDialogImpl +import xyz.alexcrea.cuanvil.update.UpdateUtils +import xyz.alexcrea.cuanvil.util.anvil.AnvilColorUtil + +object AnvilRenameDialogUtil { + + val anvilRenameDialog: AnvilRenameDialog; + + init { + val version = UpdateUtils.currentMinecraftVersion() + anvilRenameDialog = (if(!PlatformUtil.isPaper || + (version.major <= 1 && version.minor <= 21 && version.patch <= 6)) { + NoImplAnvilRenameDialog() + } else { + AnvilRenameDialogImpl({ player, component -> AnvilColorUtil.revertColorSmallest( + component, AnvilColorUtil.renamePermission(player) + ) }, + { ConfigOptions.shouldKeepRenameText }, + { ConfigOptions.renameDialogMaxSize }, + CustomAnvil.instance, + ) + }) + } + + class NoImplAnvilRenameDialog: AnvilRenameDialog { + + override fun canSendDialog(): Boolean { + return false + } + + override fun tryShowDialog( + player: HumanEntity, + event: PrepareAnvilEvent + ) {} + + override fun closeInventory(player: HumanEntity) {} + + override fun currentText(player: HumanEntity): String? { + return null + } + + override fun isOpenFor(player: HumanEntity): Boolean { + return false + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/group/AbstractMaterialGroup.kt b/src/main/kotlin/xyz/alexcrea/group/AbstractMaterialGroup.kt deleted file mode 100644 index b253da9..0000000 --- a/src/main/kotlin/xyz/alexcrea/group/AbstractMaterialGroup.kt +++ /dev/null @@ -1,50 +0,0 @@ -package xyz.alexcrea.group - -import org.bukkit.Material -import java.util.EnumSet - -abstract class AbstractMaterialGroup(private val name: String) { - protected val includedMaterial by lazy {createDefaultSet()} - - /** - * Get the group default set - */ - protected abstract fun createDefaultSet(): EnumSet - - /** - * Get if a material is allowed following the group policy - */ - fun contain(mat : Material): Boolean { - return mat in includedMaterial - } - - /** - * Get if a group is referenced by this: - */ - abstract fun isReferencing(other : AbstractMaterialGroup): Boolean - - /** - * Push a material to this group to follow this group policy - */ - abstract fun addToPolicy(mat : Material) - - /** - * Push a group to this group to follow this group policy - */ - abstract fun addToPolicy(other : AbstractMaterialGroup) - - /** - * Get the group name in case something is wrong - */ - fun getName(): String { - return name - } - - /** - * Get the group as a set - */ - fun getSet(): Set { - return includedMaterial - } - -} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/group/EnchantConflictGroup.kt b/src/main/kotlin/xyz/alexcrea/group/EnchantConflictGroup.kt deleted file mode 100644 index adef2fe..0000000 --- a/src/main/kotlin/xyz/alexcrea/group/EnchantConflictGroup.kt +++ /dev/null @@ -1,38 +0,0 @@ -package xyz.alexcrea.group - -import org.bukkit.Material -import org.bukkit.enchantments.Enchantment - -class EnchantConflictGroup(private val cantConflict: AbstractMaterialGroup, private val minBeforeBlock: Int){ - - private val enchantments = HashSet() - - fun addEnchantment(ench: Enchantment){ - enchantments.add(ench) - } - fun allowed(enchants: Set, mat: Material) : Boolean{ - if(enchantments.size < minBeforeBlock){ - return true - } - - if(cantConflict.contain(mat)){ - return true - } - - // Count the amount of enchantment that are in the list - var enchantAmount = 0 - for (enchantment in enchants) { - if(enchantment !in enchantments) continue - if(++enchantAmount > minBeforeBlock){ - return false - } - - } - return true - } - - fun getEnchants(): HashSet { - return enchantments - } - -} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/group/EnchantConflictManager.kt b/src/main/kotlin/xyz/alexcrea/group/EnchantConflictManager.kt deleted file mode 100644 index b285989..0000000 --- a/src/main/kotlin/xyz/alexcrea/group/EnchantConflictManager.kt +++ /dev/null @@ -1,149 +0,0 @@ -package xyz.alexcrea.group - -import io.delilaheve.UnsafeEnchants -import org.bukkit.Material -import org.bukkit.NamespacedKey -import org.bukkit.configuration.ConfigurationSection -import org.bukkit.configuration.file.YamlConfiguration -import org.bukkit.enchantments.Enchantment -import kotlin.collections.ArrayList - -class EnchantConflictManager { - - companion object { - // Path for the enchantments list - private const val ENCH_LIST_PATH = "enchantments" - // Path for group list related to the conflict - private const val CONFLICT_GROUP_PATH = "notAffectedGroups" - // Path for the maximum number of enchantment before validating the conflict - private const val ENCH_MAX_PATH = "maxEnchantmentBeforeConflict" - // Path for a flag: if the enchantment will be used in the last supported version - // TODO maybe replace this system by a list of "future" enchantment. - private const val FUTURE_USE_PATH = "useInFuture" - // Default name for an empty Material group - private val DEFAULT_EMPTY_GROUP = IncludeGroup("empty") - // Default name for a joining group - private const val DEFAULT_GROUP_NAME = "joinedGroup" - } - - private lateinit var conflictMap: HashMap> - - // Read and prepare all conflict - fun prepareConflicts(config: YamlConfiguration, itemManager: ItemGroupManager){ - conflictMap = HashMap() - - val keys = config.getKeys(false) - for (key in keys) { - val section = config.getConfigurationSection(key)!! - val conflict = createConflict(section,itemManager,key) - if(conflict != null){ - addToMap(conflict) - } - - } - - } - - // Add the conflict to the map - private fun addToMap(conflict: EnchantConflictGroup){ - conflict.getEnchants().forEach{ enchant -> - if(!conflictMap.containsKey(enchant)){ - conflictMap[enchant] = ArrayList() - } - conflictMap[enchant]!!.add(conflict) - } - } - - // create and read a conflict from a yaml section - private fun createConflict(section: ConfigurationSection, - itemManager: ItemGroupManager, - conflictName: String): EnchantConflictGroup? { - // Is it planed for the future - val futureUse = section.getBoolean(FUTURE_USE_PATH,false) - // Create conflict - val conflict = createConflictObject(section,itemManager,conflictName) - // Read and add enchantment to conflict - val enchantList = section.getStringList(ENCH_LIST_PATH) - for (enchantName in enchantList) { - val enchantKey = NamespacedKey.minecraft(enchantName) - val enchant = Enchantment.getByKey(enchantKey) - if(enchant == null){ - if(!futureUse){ - UnsafeEnchants.instance.logger.warning("Enchantment $enchantName do not exist but was asked for conflict $conflictName") - } - continue - } - conflict.addEnchantment(enchant) - } - if(conflict.getEnchants().size == 0){ - if(!futureUse){ - UnsafeEnchants.instance.logger.warning("Conflict $conflictName do not have valid enchantment, it will not work") - } - return null - } - - return conflict - } - - private fun createConflictObject(section: ConfigurationSection, - itemManager: ItemGroupManager, - conflictName: String): EnchantConflictGroup { - // Get the maximum number of enchantment before validating the conflict - var minBeforeBlock = section.getInt(ENCH_MAX_PATH,0) - if(minBeforeBlock < 0){ - minBeforeBlock = 0 - UnsafeEnchants.instance.logger.warning("Conflict $conflictName have an invalid value of $ENCH_MAX_PATH") - UnsafeEnchants.instance.logger.warning("It should be more or equal to 0. default to 0") - } - // Find or create the selected group for the conflict - val groupList = section.getStringList(CONFLICT_GROUP_PATH) - val finalGroup: AbstractMaterialGroup - if(groupList.size < 1){ - finalGroup = DEFAULT_EMPTY_GROUP - }else if(groupList.size == 1){ - finalGroup = findGroup(groupList[0], itemManager, conflictName) - }else{ - finalGroup = IncludeGroup(DEFAULT_GROUP_NAME) - for (groupName in groupList) { - finalGroup.addToPolicy(findGroup(groupName, itemManager, conflictName)) - } - } - // Return conflict - return EnchantConflictGroup(finalGroup, minBeforeBlock) - } - - private fun findGroup(groupName: String,itemManager: ItemGroupManager, conflictName: String): AbstractMaterialGroup { - val group = itemManager.get(groupName) - if(group == null){ - UnsafeEnchants.instance.logger.warning("Group $groupName do not exist but is ask by conflict $conflictName") - return DEFAULT_EMPTY_GROUP - } - - return group - } - - fun isConflicting(base: Set,mat: Material, newEnchant: Enchantment): ConflictType{ - val conflictList = conflictMap[newEnchant] ?: return ConflictType.NO_CONFLICT - - var result = ConflictType.NO_CONFLICT - for (conflict in conflictList) { - if(!conflict.allowed(base,mat)) { - if(conflict.getEnchants().size <= 1){ - result = ConflictType.SMALL_CONFLICT - }else{ - return ConflictType.BIG_CONFLICT - } - } - } - return result - } - -} - -enum class ConflictType(){ - NO_CONFLICT, - SMALL_CONFLICT, - BIG_CONFLICT - - -} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/group/ExcludeGroup.kt b/src/main/kotlin/xyz/alexcrea/group/ExcludeGroup.kt deleted file mode 100644 index e929346..0000000 --- a/src/main/kotlin/xyz/alexcrea/group/ExcludeGroup.kt +++ /dev/null @@ -1,33 +0,0 @@ -package xyz.alexcrea.group - -import org.bukkit.Material -import java.util.* -import kotlin.collections.HashSet - -class ExcludeGroup(name: String): AbstractMaterialGroup(name) { - override fun createDefaultSet(): EnumSet { - return EnumSet.allOf(Material::class.java) - } - - private val includedGroup = HashSet() - - override fun isReferencing(other: AbstractMaterialGroup): Boolean { - for (materialGroup in includedGroup.iterator()) { - if((materialGroup == other) || (materialGroup.isReferencing(other))){ - return true - } - } - return false - } - - override fun addToPolicy(mat: Material) { - includedMaterial.remove(mat) - } - - override fun addToPolicy(other: AbstractMaterialGroup) { - includedGroup.add(other) - includedMaterial.removeAll(other.getSet()) - } - - -} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/group/IncludeGroup.kt b/src/main/kotlin/xyz/alexcrea/group/IncludeGroup.kt deleted file mode 100644 index d349821..0000000 --- a/src/main/kotlin/xyz/alexcrea/group/IncludeGroup.kt +++ /dev/null @@ -1,31 +0,0 @@ -package xyz.alexcrea.group - -import org.bukkit.Material -import java.util.EnumSet - -class IncludeGroup(name: String): AbstractMaterialGroup(name) { - override fun createDefaultSet(): EnumSet { - return EnumSet.noneOf(Material::class.java) - } - - private val includedGroup = HashSet() - - override fun isReferencing(other: AbstractMaterialGroup): Boolean { - for (materialGroup in includedGroup.iterator()) { - if((materialGroup == other) || (materialGroup.isReferencing(other))){ - return true - } - } - return false - } - - override fun addToPolicy(mat: Material) { - includedMaterial.add(mat) - } - - override fun addToPolicy(other: AbstractMaterialGroup) { - includedGroup.add(other) - includedMaterial.addAll(other.getSet()) - } - -} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 19a97ab..7d6e396 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,38 +1,123 @@ -# Whether all anvil actions should be capped # -# If true, all anvil repairs will max out at the value of limit_repair_value -limit_repair_cost: true +# It is recommended that you use /configanvil to edit most of these config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# -# Value to limit repair costs to when limit_repair_cost is true +# What service of metric should custom anvil use +# Custom anvil collect generic information like server minecraft version, type, etc... +# It can also collect error information if error is happening (currently faststats only) +# It can also be disabled +# Please refer to README for public metric link +# Possible options: auto, bstat, faststats, disabled (auto by default) +metric_type: auto + +# Allow to report errors made caused by this plugin (only for faststats) +# This allows me to fix potentials issue that I'm not aware of +# Accept true or false (true by default) +metric_collect_errors: true + +# All anvil cost will be capped to limit_repair_value if enabled. # -# Valid range of 1 - 39 (vanilla will consider 40+ as "too expensive") +# In other words: +# For any anvil cost greater than limit_repair_value, Cost will be set to limit_repair_value. +limit_repair_cost: false + +# Max cost value the Anvil can get to. +# +# Valid values include 0 to 1000. +# Cost will be displayed as "Too Expensive": +# - If Cost is above 39 +# - And replace_too_expensive is disabled (false) limit_repair_value: 39 -# Whether the anvil's repair limit should be removed entirely +# Whether the anvil's cost limit should be removed entirely. # -# The anvil will still visually display "too expensive" however the action will be completable +# The anvil will still visually display "Too Expensive" if "replace_too_expensive" is disabled +# However, the action will be completable if xp requirement is meet. remove_repair_limit: false -# Value added to the anvil when the item durability increase +# Whenever anvil cost is above 39 should display the true price and not "Too Expensive". # -# Valid range of 0 - 255 +# However, when bypassing "Too Expensive", anvil price will be displayed as Green. +# If the action is not completable, the cost will still be displayed as "Too expensive". +# That mean you also need to change other settings like remove_repair_limit or limit_repair_cost. +# +# Require ProtocoLib. +replace_too_expensive: false + +# XP Level amount added to the anvil when the item is repaired by another item of the same type +# +# Valid values include 0 to 1000 item_repair_cost: 2 -# Value added to the anvil when the item is renamed +# XP Level amount added to the anvil when the item is renamed # -# Valid range of 0 - 255 +# Valid values include 0 to 1000 item_rename_cost: 1 -# Value added to the anvil when a sacrifice enchantment conflict -# with one of the left item enchantment +# XP Level amount added to the anvil when the item is repaired by an "unit" +# For example: a Diamond on a Diamond Sword +# What's considered unit for what can be edited on the unit repair configuration. # -# Valid range of 0 - 255 +# Valid values include 0 to 1000 +unit_repair_cost: 1 + +# XP Level amount added to the anvil when a sacrifice enchantment +# conflict with one of the left item enchantment +# +# Valid values include 0 to 1000 sacrifice_illegal_enchant_cost: 1 -# Default limit to apply to any enchants missing from override_limits +# Allow using color code and hexadecimal color. # -# Valid range of 1 - 255 -default_limit: 5 +# Color code are prefixed by "&" and hexadecimal color by "#". +# Color code will not be applied if it colors nothing. "&&" can be used to write "&". +# For minimessage search for minimessage formating https://docs.papermc.io/adventure/minimessage/format/ +# Note that only color and decoration tags are allowed for minimisage in the v1 version of this plugin +# but any global tag will be allowed later when v2 release +allow_color_code: false +allow_hexadecimal_color: false +allow_minimessage: false + +# This enables restricting color code for player having specific permission +# It requires allow_color_code enabled for... obvious reasons +# +# For example: if player want to use "&aHello" it will be required that the player has +# the permission "ca.color.code.a" as he used the color code "a" +# In general permission to give to the player is "ca.color.code.[code]" +# where [code] is the color code you wish to allow the player +# +# It is kinda of useless when minimessage is supported as players would be able to bypass +# that using the equivalent minimessage tag +per_color_code_permission: false + +# Toggle if color should only be applicable if the player a certain permission. +# +# permission are "ca.color.code" for use of color code and "ca.color.hex" for use of hexadecimal color. +permission_needed_for_color: true + +# Xp cost if the player use color in the items name on rename. +# +# Valid values include 0 to 1000. +use_of_color_cost: 0 + +# Dialogue rename menu make use of dialog menu to allow bigger rename +# You can also change the maximum size and set it to -1 or less for maximum +# +# This feature only work on paper 1.21.7 or later +# +# At the moment only english is available for this menu... sorry ! +# +# CustomAnvil use "ca.rename.dialog" when permission +enable_dialog_rename: false +dialog_rename_max_size: 256 +permission_needed_for_dialog_rename: false + +# This allows custom anvil to not "guess" the text used for rename but store it in the item +# It will make item stackable only and only if it had used the same rename text +# +# For practical reason. this only work when dialog rename is enabled +dialog_rename_keep_user_text: true # Override limits for specific enchants # @@ -40,47 +125,49 @@ default_limit: 5 # # Overrides provided default from aqua_affinity to depth_strider won't change effect with extra levels # -# Valid range of 1 - 255 for each enchantment +# Valid range of 0 - 255 for each enchantment +# -1 mean keep default enchant_limits: - aqua_affinity: 1 - binding_curse: 1 - channeling: 1 - flame: 1 - infinity: 1 - mending: 1 - multishot: 1 - silk_touch: 1 - vanishing_curse: 1 - depth_strider: 3 # anything more than 3 is treated as 3 by the game - protection: 4 - fire_protection: 4 - blast_protection: 4 - projectile_protection: 4 - feather_falling: 4 - thorns: 3 - respiration: 3 - sharpness: 5 - smite: 5 - bane_of_arthropods: 5 - knockback: 2 - fire_aspect: 2 - looting: 3 - sweeping: 3 - efficiency: 5 - unbreaking: 3 - fortune: 3 - power: 5 - punch: 2 - luck_of_the_sea: 3 - lure: 3 - frost_walker: 2 - impaling: 5 - riptide: 3 - loyalty: 3 - piercing: 4 - quick_charge: 3 - soul_speed: 3 - swift_sneak: 3 + minecraft:aqua_affinity: 1 + minecraft:binding_curse: 1 + minecraft:channeling: 1 + minecraft:flame: 1 + minecraft:infinity: 1 + minecraft:mending: 1 + minecraft:multishot: 1 + minecraft:silk_touch: 1 + minecraft:vanishing_curse: 1 + minecraft:depth_strider: 3 # anything more than 3 is treated as 3 by the game + minecraft:protection: 4 + minecraft:fire_protection: 4 + minecraft:blast_protection: 4 + minecraft:projectile_protection: 4 + minecraft:feather_falling: 4 + minecraft:thorns: 3 + minecraft:respiration: 3 + minecraft:sharpness: 5 + minecraft:smite: 5 + minecraft:bane_of_arthropods: 5 + minecraft:knockback: 2 + minecraft:fire_aspect: 2 + minecraft:looting: 3 + minecraft:sweeping: 3 + minecraft:sweeping_edge: 3 + minecraft:efficiency: 5 + minecraft:unbreaking: 3 + minecraft:fortune: 3 + minecraft:power: 5 + minecraft:punch: 2 + minecraft:luck_of_the_sea: 3 + minecraft:lure: 3 + minecraft:frost_walker: 2 + minecraft:impaling: 5 + minecraft:riptide: 3 + minecraft:loyalty: 3 + minecraft:piercing: 4 + minecraft:quick_charge: 3 + minecraft:soul_speed: 3 + minecraft:swift_sneak: 3 # Multipliers used to calculate the enchantment's value in repair/combining # @@ -94,123 +181,289 @@ enchant_limits: # With default values protection 4 would have a value of 4 when # coming from either a book (4 * 1) or an item (4 * 1) enchant_values: - aqua_affinity: + minecraft:aqua_affinity: item: 4 book: 2 - bane_of_arthropods: + minecraft:bane_of_arthropods: item: 2 book: 1 - binding_curse: + minecraft:binding_curse: item: 8 book: 4 - blast_protection: + minecraft:blast_protection: item: 4 book: 2 - channeling: + minecraft:channeling: item: 8 book: 4 - depth_strider: + minecraft:depth_strider: item: 4 book: 2 - efficiency: + minecraft:efficiency: item: 1 book: 1 - flame: + minecraft:flame: item: 4 book: 2 - feather_falling: + minecraft:feather_falling: item: 2 book: 1 - fire_aspect: + minecraft:fire_aspect: item: 4 book: 2 - fire_protection: + minecraft:fire_protection: item: 2 book: 1 - fortune: + minecraft:fortune: item: 4 book: 2 - frost_walker: + minecraft:frost_walker: item: 4 book: 2 - impaling: + minecraft:impaling: item: 4 book: 2 - infinity: + minecraft:infinity: item: 8 book: 4 - knockback: + minecraft:knockback: item: 2 book: 1 - looting: + minecraft:looting: item: 4 book: 2 - loyalty: + minecraft:loyalty: item: 1 book: 1 - luck_of_the_sea: + minecraft:luck_of_the_sea: item: 4 book: 2 - lure: + minecraft:lure: item: 4 book: 2 - mending: + minecraft:mending: item: 4 book: 2 - multishot: + minecraft:multishot: item: 4 book: 2 - piercing: + minecraft:piercing: item: 1 book: 1 - power: + minecraft:power: item: 1 book: 1 - projectile_protection: + minecraft:projectile_protection: item: 2 book: 1 - protection: + minecraft:protection: item: 1 book: 1 - punch: + minecraft:punch: item: 4 book: 2 - quick_charge: + minecraft:quick_charge: item: 2 book: 1 - respiration: + minecraft:respiration: item: 4 book: 2 - riptide: + minecraft:riptide: item: 4 book: 2 - silk_touch: + minecraft:silk_touch: item: 8 book: 4 - sharpness: + minecraft:sharpness: item: 1 book: 1 - smite: + minecraft:smite: item: 2 book: 1 - soul_speed: + minecraft:soul_speed: item: 8 book: 4 - swift_sneak: + minecraft:swift_sneak: item: 8 book: 4 - sweeping: + minecraft:sweeping: item: 4 book: 2 - thorns: + minecraft:sweeping_edge: + item: 4 + book: 2 + minecraft:thorns: item: 8 book: 4 - unbreaking: + minecraft:unbreaking: item: 2 book: 1 - vanishing_curse: + minecraft:vanishing_curse: item: 8 book: 4 +# Disable enchantment merging for level above the set value +# Enchantment merging is when, for example, 2 unbreaking II book combine to give sharpness III +# But Enchantment above this value can still be applied. following the previous example, we could still apply a unbreaking III book to a sword +# Even if disable-merge-over of unbreaking is set to 2 +# -1 mean enchantment merge for this enchantment is not disabled. default to -1 if absent. +disable-merge-over: + # Sharpness is set to -1. it equivalent to it not being set to anything (and work as vanilla on default configuration) + minecraft:sharpness: -1 + # If uncommented. 2 unbreaking II book would not give an unbreaking III book. but unbreaking III book can still be applied + #minecraft:unbreaking: 2 + +# The maximum number of enchantment an item can get. -1 for infinity +# Use eco enchant enchant_limit if present by default unless "default" is not equal to -1 +enchantment_count_limit: + default: -1 + # Limit for specific items. example bellow is an example with stick + # Per item enchantment limit override eco enchant enchant_limit and default limit + items: + stick: -1 + +# Settings for lore modification +lore_edit: + book_and_quil: + # Permission is ca.lore_edit.book + use_permission: true + append: + # If adding lore using book & quil is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Cost used for every lore line added + per_line_cost: 0 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If adding the lore consume the book & quil + do_consume: false + # Allow using color code and hexadecimal color when editing lore via book & quil + # + # Color code are prefixed by "&" and hexadecimal color by "#" + # Color code will not be applied if it colors nothing. "&&" can be used to write "&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + # + # Note that currently minimessage would disable hex code when adding color + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + + remove: + # If removing lore using book & quil is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Cost used for every lore line removed + per_line_cost: 0 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If removing the lore consume the book & quil + do_consume: false + # Cost of replacing colors + remove_color_cost: 0 + # Allowed some color and tags to be reverted to plain text + # Custom anvil will prioritise format that result is a smaller resulting text + # Note that not allowing certain format will lead to some lost of color or tags. + # If configuration are exact as append appending this book should result in the exact same color + # + # Color code will be prefixed by "&" and hexadecimal color by "#". + # If color code is allowed, "&" in the text will get converted to "&&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + + paper: + # Permission is ca.lore_edit.paper + use_permission: true + # what order should the lines should get added/removed (start/end, if invalid or not present will be end) + order: end + + append_line: + # If adding lore line using paper is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If adding the lore line consume the paper + do_consume: false + # Allow using color code and hexadecimal color when editing lore via book & quil + # + # Color code are prefixed by "&" and hexadecimal color by "#" + # Color code will not be applied if it colors nothing. "&&" can be used to write "&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + # + # Note that currently minimessage would disable hex code when adding color + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + color_use_cost: 0 + + remove_line: + # If removing lore line using paper is enabled + enabled: false + # Cost used every time + fixed_cost: 1 + # Use left item vanilla cost penalty if any + shared_increase: false + # Increase shared left item cost penalty + shared_additive: false + # If removing the lore line consume the paper + do_consume: false + # Cost of replacing colors + remove_color_cost: 0 + # Allowed some color and tags to be reverted to plain text + # Custom anvil will prioritise format that result is a smaller resulting text + # Note that not allowing certain format will lead to some lost of color or tags. + # If configuration are exact as append appending this paper should result in the exact same color + # + # Color code will be prefixed by "&" and hexadecimal color by "#". + # If color code is allowed, "&" in the text will get converted to "&&" + # For minimessage see minimessage formating https://docs.papermc.io/adventure/minimessage/format/ + allow_color_code: true + allow_hexadecimal_color: false + allow_minimessage: true + +# Allow to replace the xp cost by a monetary cost +# If enabled it will not be bound to the experience level limits +# +# It also requires to enable dialog rename (set "enable_dialog_rename: false" a bit higher) +# If dialog rename permission is enabled and player do not have the permission merge will fall back to vanilla xp cost +# +# If you are using custom craft I recommend using Linear Xp Cost with Exact Linear Xp as normal Xp Cost will act "weird" +# But Linear Xp will act as 1$ time global multiplier. In other word: like you expect +# +# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 and later +monetary_cost: + enabled: false + # If using vault unlocked this allow to specify what currency should be used for anvil usage + # default being the default currency + currency: default + # multiply the anvil cost by a value to allow to have price a big bigger than like 40 + multipliers: + # global multipliers. all usage type will be multiplied by this value + global: 1.0 + # usage specific type. it will only apply for specific xp "reason" + enchantment: 1.0 # related to enchantments level + repair: 1.0 # for repairing via unit repair (per unit) + rename: 1.0 # for renaming the item + lore_edit: 1.0 # for changing the lore of the item (only if lore edit is enabled) + illegal_penalty: 1.0 # for trying to combine illegal enchantment + work_penalty: 1.0 # for work penalty (aka use penalty) + recipe: 1.0 # for custom anvil recipe cost + # Whether to show debug logging debug_log: false + +# Whether to show verbose debug logging +debug_log_verbose: false + +configVersion: 1.11.0 diff --git a/src/main/resources/custom_recipes.yml b/src/main/resources/custom_recipes.yml new file mode 100644 index 0000000..57c2220 --- /dev/null +++ b/src/main/resources/custom_recipes.yml @@ -0,0 +1,5 @@ +# ---------------------------------------------------- +# This config file is to store custom craft +# It is recommended to use the in game config editor for this configuration. +# /customanvilconfig With ca.config.edit permission +# ---------------------------------------------------- diff --git a/src/main/resources/datapack/bracken/enchant_conflict.yml b/src/main/resources/datapack/bracken/enchant_conflict.yml new file mode 100644 index 0000000..851a398 --- /dev/null +++ b/src/main/resources/datapack/bracken/enchant_conflict.yml @@ -0,0 +1,35 @@ +"bracken:abstraction": ['#sword_enchant_conflict'] +"bracken:antivenom": ['#protection_enchant_conflict'] +"bracken:blinding_fix": ['minecraft:fire_aspect'] +"bracken:boldness": ['#protection_enchant_conflict'] +"bracken:butchering": ['#sword_enchant_conflict'] +"bracken:defusing": ['#sword_enchant_conflict'] +"bracken:dentine_touch": ['#tool_conflict'] +"bracken:dullness_curse": ['minecraft:sharpness'] +"bracken:famine_walker": ['#boot_conflict'] +"bracken:ferocity": ['#sword_enchant_conflict'] +"bracken:flame_walker": ['#boot_conflict'] +"bracken:float_walker": ['#boot_conflict'] +"bracken:flood_walker": ['#boot_conflict'] +"bracken:fragility_curse": ['minecraft:unbreaking'] +"bracken:freezing_fix": ['minecraft:fire_aspect'] +"bracken:guarding": ['#sword_enchant_conflict'] +"bracken:huskiness": ['#bracken_pet_armor'] +"bracken:infused_fire_aspect_fix": ['minecraft:fire_aspect'] +"bracken:infused_frost_walker_fix": ['#boot_conflict'] +"bracken:infused_mending_fix": ['minecraft:mending'] +"bracken:infused_thorns_fix": ['minecraft:thorns'] +"bracken:lethargy_curse": ['bracken:ferocity'] +"bracken:lifesteal": ['#bow_conflict'] +"bracken:litheness": ['#bracken_pet_armor'] +"bracken:lunar_concordance_armor": ['#bracken_lunarite_armor'] +"bracken:lunar_concordance_weapon": ['#bracken_lunarite_weapons'] +"bracken:maiming_curse": ['bracken:vitality_fix'] +"bracken:mortality": ['#sword_enchant_conflict'] +"bracken:poisoning_fix": ['minecraft:fire_aspect'] +"bracken:pugnacity": ['#bracken_pet_armor'] +"bracken:rending": ['#crossbow_conflict'] +"bracken:silvered_fix": ['#all'] +"bracken:vitality_fix": ['bracken:maiming_curse'] +"bracken:weakening_fix": ['minecraft:fire_aspect'] +"bracken:withering_fix": ['minecraft:fire_aspect'] diff --git a/src/main/resources/datapack/bracken/item_conflict.yml b/src/main/resources/datapack/bracken/item_conflict.yml new file mode 100644 index 0000000..1c4d9da --- /dev/null +++ b/src/main/resources/datapack/bracken/item_conflict.yml @@ -0,0 +1,54 @@ +"bracken:abstraction": ['melee_weapons', 'mace'] +"bracken:antivenom": ['armors'] +"bracken:astuteness_fix": ['armors'] +"bracken:blinding_fix": ['can_unbreak'] +"bracken:boldness": ['armors'] +"bracken:butchering": ['melee_weapons', 'mace'] +"bracken:chiseling_fix": ['axes', 'pickaxes', 'shovels', 'hoes'] +"bracken:decaying_fix": ['can_unbreak'] +"bracken:defusing": ['melee_weapons', 'mace'] +"bracken:dentine_touch": ['axes', 'pickaxes', 'shovels', 'hoes'] +"bracken:devouring_curse": ['melee_weapons', 'mace'] +"bracken:dullness_curse": ['melee_weapons', 'mace'] +"bracken:famine_walker": ['boots'] +"bracken:ferocity": ['melee_weapons', 'mace'] +"bracken:flame_walker": ['boots'] +"bracken:float_walker": ['boots'] +"bracken:flood_walker": ['boots'] +"bracken:fragility_curse": ['can_unbreak'] +"bracken:freezing_fix": ['can_unbreak'] +"bracken:guarding": ['melee_weapons', 'mace'] +"bracken:huskiness": ['pet_armor'] +"bracken:infused_fire_aspect_fix": ['melee_weapons', 'mace'] +"bracken:infused_frost_walker_fix": ['boots'] +"bracken:infused_mending_fix": ['can_unbreak'] +"bracken:infused_thorns_fix": ['armors'] +"bracken:integrity_fix": ['armors'] +"bracken:lethargy_curse": ['melee_weapons', 'mace'] +"bracken:lifesteal": ['bow'] +"bracken:litheness": ['pet_armor'] +"bracken:maiming_curse": ['armors'] +"bracken:mortality": ['melee_weapons', 'mace'] +"bracken:poisoning_fix": ['can_unbreak'] +"bracken:pugnacity": ['wolf_armor'] +"bracken:pushback": ['armors'] +"bracken:quenching_fix": ['armors'] +"bracken:rending": ['crossbow'] +"bracken:reverse_thorns": ['axes'] +"bracken:searing_surface": ['armors'] +"bracken:sentience_curse_1": ['can_unbreak'] +"bracken:sentience_curse_2": ['can_unbreak'] +"bracken:sentience_curse_3": ['can_unbreak'] +"bracken:sentience_curse_4": ['can_unbreak'] +"bracken:sentience_curse_5": ['can_unbreak'] +"bracken:sentience_curse_6": ['can_unbreak'] +"bracken:sentience_curse_7": ['can_unbreak'] +"bracken:shrewdness": ['swords'] +"bracken:silvered_fix": ['can_unbreak'] +"bracken:spectrality_fix": ['melee_weapons', 'mace'] +"bracken:splintering": ['bow'] +"bracken:trampling": ['horse_armor'] +"bracken:vitality_fix": ['armors'] +"bracken:weakening_fix": ['can_unbreak'] +"bracken:wisdom": ['tools'] +"bracken:withering_fix": ['can_unbreak'] diff --git a/src/main/resources/datapack/bracken/item_groups.yml b/src/main/resources/datapack/bracken/item_groups.yml new file mode 100644 index 0000000..8248f0c --- /dev/null +++ b/src/main/resources/datapack/bracken/item_groups.yml @@ -0,0 +1,15 @@ +horse_armor: + items: + - leather_horse_armor + - iron_horse_armor + - golden_horse_armor + - diamond_horse_armor + +wolf_armor: + items: + - wolf_armor + +pet_armor: + groups: + - wolf_armor + - horse_armor \ No newline at end of file diff --git a/src/main/resources/datapack/dungeons_and_taverns/enchant_conflict.yml b/src/main/resources/datapack/dungeons_and_taverns/enchant_conflict.yml new file mode 100644 index 0000000..5ebd042 --- /dev/null +++ b/src/main/resources/datapack/dungeons_and_taverns/enchant_conflict.yml @@ -0,0 +1,21 @@ +"nova_structures:power": ['#dungeon_at_might', '#dungeon_at_powah'] +"nova_structures:ghasted": ['#crossbow_conflict'] +"nova_structures:gravity": ['#crossbow_conflict'] +"nova_structures:antidote": ['#protection_enchant_conflict'] +"nova_structures:piercing": ['#dungeon_at_structure_bow'] +"nova_structures:traveler": ['#dungeon_at_boots'] +"nova_structures:multishot": ['#dungeon_at_structure_bow'] +"nova_structures:wax_wings": ['#dungeon_at_elytra'] +"nova_structures:aerials_bane": ['#dungeon_at_powah', '#sword_enchant_conflict'] +"nova_structures:shulker_boss": ['minecraft:unbreaking'] +"nova_structures:illagers_bane": ['#sword_enchant_conflict'] +"nova_structures:wither_coated": ['#dungeon_at_sword_effect'] +"nova_structures:boss_behaviour": ['minecraft:unbreaking'] +"nova_structures:photosynthesis": ['#dungeon_at_repair'] +"nova_structures:shulker_miniboss": ['minecraft:unbreaking'] +"minecraft:soul_speed": ['#dungeon_at_boots'] +"minecraft:mending": ['#dungeon_at_repair'] +"minecraft:power": ['#dungeon_at_powah'] +"minecraft:unbreaking": ['#dungeon_at_elytra'] +"minecraft:infinity": ['#dungeon_at_structure_bow'] +"minecraft:fire_aspect": ['#dungeon_at_sword_effect'] diff --git a/src/main/resources/datapack/dungeons_and_taverns/item_conflict.yml b/src/main/resources/datapack/dungeons_and_taverns/item_conflict.yml new file mode 100644 index 0000000..d98c5dd --- /dev/null +++ b/src/main/resources/datapack/dungeons_and_taverns/item_conflict.yml @@ -0,0 +1,18 @@ +"nova_structures:power": ['crossbow'] +"nova_structures:ghasted": ['crossbow'] +"nova_structures:gravity": ['crossbow'] +"nova_structures:antidote": ['chestplate'] +"nova_structures:outreach": ['chestplate'] +"nova_structures:piercing": ['bow'] +"nova_structures:spiteful": ['melee_weapons'] +"nova_structures:traveler": ['boots'] +"nova_structures:multishot": ['bow'] +"nova_structures:wax_wings": ['elytra'] +"nova_structures:aerials_bane": ['combat_tools'] +"nova_structures:shulker_boss": ['stick'] +"nova_structures:illagers_bane": ['melee_weapons'] +"nova_structures:wither_coated": ['mace', 'melee_weapons'] +"nova_structures:boss_behaviour": ['stick', 'flint'] +"nova_structures:photosynthesis": ['can_unbreak'] +"nova_structures:shulker_miniboss": ['stick'] +"nova_structures:conductivity_curse": ['metal'] diff --git a/src/main/resources/datapack/dungeons_and_taverns/item_groups.yml b/src/main/resources/datapack/dungeons_and_taverns/item_groups.yml new file mode 100644 index 0000000..45fc3b7 --- /dev/null +++ b/src/main/resources/datapack/dungeons_and_taverns/item_groups.yml @@ -0,0 +1,56 @@ +metal: + type: include + items: + - shield + - trident + - flint_and_steel + - shears + - brush + - mace + - chainmail_chestplate + - golden_chestplate + - iron_chestplate + - netherite_chestplate + - chainmail_boots + - golden_boots + - iron_boots + - netherite_boots + - chainmail_leggings + - golden_leggings + - iron_leggings + - netherite_leggings + - chainmail_helmet + - golden_helmet + - iron_helmet + - netherite_helmet + - golden_pickaxe + - iron_pickaxe + - netherite_pickaxe + - golden_axe + - iron_axe + - netherite_axe + - golden_shovel + - iron_shovel + - netherite_shovel + - golden_sword + - iron_sword + - netherite_sword + - golden_hoe + - iron_hoe + - netherite_hoe + - trial_key + - ominous_trial_key + +combat_tools: + type: include + groups: + - melee_weapons + - bow + - crossbow + - trident + - mace + +flint: + type: include + items: + - flint \ No newline at end of file diff --git a/src/main/resources/datapack/enchantplus/enchant_conflict.yml b/src/main/resources/datapack/enchantplus/enchant_conflict.yml new file mode 100644 index 0000000..a8e3e61 --- /dev/null +++ b/src/main/resources/datapack/enchantplus/enchant_conflict.yml @@ -0,0 +1,40 @@ +"enchantplus:bow/echo_shot": ['#enchantplus_bow'] +"enchantplus:bow/storm_arrow": ['#enchantplus_bow'] +"enchantplus:bow/eternal_frost": ['#enchantplus_bow'] +"enchantplus:bow/breezing_arrow": ['#enchantplus_bow'] +"enchantplus:bow/explosive_arrow": ['#enchantplus_bow'] +"enchantplus:mace/teluric_wave": ['#enchantplus_mace'] +"enchantplus:armor/fury": ['#enchantplus_armor'] +"enchantplus:armor/lifeplus": ['#enchantplus_armor'] +"enchantplus:armor/venom_protection": ['#protection_enchant_conflict'] +"enchantplus:boots/lava_walker": ['#boot_conflict'] +"enchantplus:boots/step_assist": ['#boot_conflict'] +"enchantplus:sword/fear": ['#enchantplus_sword_effect'] +"enchantplus:sword/pull": ['#enchantplus_sword_effect'] +"enchantplus:sword/reach": ['#enchantplus_sword_attribute'] +"enchantplus:sword/critical": ['#enchantplus_sword_attribute'] +"enchantplus:sword/xp_boost": ['#enchantplus_experience'] +"enchantplus:sword/last_hope": ['#enchantplus_sword_effect'] +"enchantplus:sword/life_steal": ['#enchantplus_sword_effect'] +"enchantplus:sword/death_touch": ['#enchantplus_sword_effect'] +"enchantplus:sword/attack_speed": ['#enchantplus_sword_attribute'] +"enchantplus:sword/poison_aspect": ['#enchantplus_aspect'] +"enchantplus:sword/runic_despair": ['#enchantplus_sword_attribute'] +"enchantplus:sword/dimensional_hit": ['#enchantplus_sword_attribute'] +"enchantplus:sword/tears_of_asflors": ['#enchantplus_sword_effect'] +"enchantplus:tools/auto_smelt": ['minecraft:silk_touch'] +"enchantplus:tools/miningplus": ['#enchantplus_mining'] +"enchantplus:pickaxe/vein_miner": ['#enchantplus_mining'] +"enchantplus:pickaxe/spawner_touch": ['#enchantplus_mining'] +"enchantplus:pickaxe/bedrock_breaker": ['#enchantplus_mining'] +"enchantplus:trident/gungnir_breath": ['#enchantplus_trident'] +"enchantplus:leggings/dwarfed": ['#enchantplus_size'] +"enchantplus:leggings/oversize": ['#enchantplus_size'] +"enchantplus:durability/curse_of_enchant": ['#enchantplus_durability', 'minecraft:unbreaking', 'minecraft:mending'] +"enchantplus:durability/curse_of_breaking": ['#enchantplus_durability', 'minecraft:unbreaking', 'minecraft:mending'] +"minecraft:protection": ['#enchantplus_armor'] +"minecraft:quick_charge": ['#enchantplus_bow'] +"minecraft:mending": ['#enchantplus_experience'] +"minecraft:fire_aspect": ['#enchantplus_aspect'] +"minecraft:channeling": ['#enchantplus_trident'] +"minecraft:wind_burst": ['#enchantplus_mace'] diff --git a/src/main/resources/datapack/enchantplus/item_conflict.yml b/src/main/resources/datapack/enchantplus/item_conflict.yml new file mode 100644 index 0000000..8dafc57 --- /dev/null +++ b/src/main/resources/datapack/enchantplus/item_conflict.yml @@ -0,0 +1,55 @@ +"enchantplus:axe/timber": ['axes'] +"enchantplus:bow/rebound": ['bow', 'crossbow'] +"enchantplus:bow/echo_shot": ['bow', 'crossbow'] +"enchantplus:bow/storm_arrow": ['bow', 'crossbow'] +"enchantplus:bow/accuracy_shot": ['bow', 'crossbow'] +"enchantplus:bow/eternal_frost": ['bow', 'crossbow'] +"enchantplus:bow/breezing_arrow": ['bow', 'crossbow'] +"enchantplus:bow/explosive_arrow": ['bow', 'crossbow'] +"enchantplus:hoe/harvest": ['hoes'] +"enchantplus:hoe/scyther": ['hoes'] +"enchantplus:mace/striker": ['mace'] +"enchantplus:mace/teluric_wave": ['mace'] +"enchantplus:mace/wind_propulsion": ['mace'] +"enchantplus:armor/fury": ['armors'] +"enchantplus:armor/lifeplus": ['armors'] +"enchantplus:armor/venom_protection": ['armors'] +"enchantplus:boots/agility": ['boots'] +"enchantplus:boots/lava_walker": ['boots'] +"enchantplus:boots/step_assist": ['boots'] +"enchantplus:sword/fear": ['swords'] +"enchantplus:sword/pull": ['melee_weapons'] +"enchantplus:sword/reach": ['swords'] +"enchantplus:sword/critical": ['swords'] +"enchantplus:sword/xp_boost": ['mining_and_damage'] +"enchantplus:sword/last_hope": ['swords'] +"enchantplus:sword/life_steal": ['melee_weapons'] +"enchantplus:sword/death_touch": ['melee_weapons'] +"enchantplus:sword/attack_speed": ['swords'] +"enchantplus:sword/poison_aspect": ['melee_weapons'] +"enchantplus:sword/runic_despair": ['swords'] +"enchantplus:sword/dimensional_hit": ['swords'] +"enchantplus:sword/tears_of_asflors": ['swords'] +"enchantplus:tools/auto_smelt": ['axes', 'pickaxes', 'shovels', 'hoes'] +"enchantplus:tools/miningplus": ['axes', 'pickaxes', 'shovels', 'hoes'] +"enchantplus:elytra/armored": ['elytra'] +"enchantplus:elytra/kinetic_protection": ['elytra'] +"enchantplus:helmet/voidless": ['helmets'] +"enchantplus:helmet/auto_feed": ['helmets'] +"enchantplus:helmet/bright_vision": ['helmets'] +"enchantplus:mounted/velocity": ['mounted_armor'] +"enchantplus:mounted/steel_fang": ['wolf_armor'] +"enchantplus:mounted/cavalier_egis": ['mounted_armor'] +"enchantplus:mounted/ethereal_leap": ['mounted_armor'] +"enchantplus:pickaxe/vein_miner": ['pickaxes'] +"enchantplus:pickaxe/spawner_touch": ['pickaxes'] +"enchantplus:pickaxe/bedrock_breaker": ['pickaxes'] +"enchantplus:trident/gungnir_breath": ['trident'] +"enchantplus:leggings/dwarfed": ['leggings'] +"enchantplus:leggings/leaping": ['leggings'] +"enchantplus:leggings/oversize": ['leggings'] +"enchantplus:leggings/fast_swim": ['leggings'] +"enchantplus:chestplate/builder_arm": ['chestplate'] +"enchantplus:durability/curse_of_enchant": ['can_unbreak'] +"enchantplus:durability/curse_of_breaking": ['can_unbreak'] +"enchantplus:midas_touch": ['stick'] diff --git a/src/main/resources/datapack/enchantplus/item_groups.yml b/src/main/resources/datapack/enchantplus/item_groups.yml new file mode 100644 index 0000000..74854de --- /dev/null +++ b/src/main/resources/datapack/enchantplus/item_groups.yml @@ -0,0 +1,30 @@ +mining_and_damage: + groups: + - melee_weapons + - mace + - bow + - crossbow + - mace + - trident + - tools + +stick: + items: + - stick + +wolf_armor: + items: + - wolf_armor + +mounted_armor: + items: + - diamond_horse_armor + - golden_horse_armor + - iron_horse_armor + - leather_horse_armor + groups: + - wolf_armor + +elytra: + items: + - elytra \ No newline at end of file diff --git a/src/main/resources/enchant_conflict.yml b/src/main/resources/enchant_conflict.yml index c5fea81..45d62c3 100644 --- a/src/main/resources/enchant_conflict.yml +++ b/src/main/resources/enchant_conflict.yml @@ -1,3 +1,8 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + # material conflicts # # If you want to edit this file: @@ -12,159 +17,163 @@ # ---------------------------------------------------- restriction_aqua_affinity: - enchantments: [aqua_affinity] - notAffectedGroups: [enchanted_book, helmets] + enchantments: [ minecraft:aqua_affinity ] + notAffectedGroups: [ enchanted_book, helmets ] restriction_bane_of_arthropods: - enchantments: [bane_of_arthropods] - notAffectedGroups: [enchanted_book, melee_weapons] + enchantments: [ minecraft:bane_of_arthropods ] + notAffectedGroups: [ enchanted_book, melee_weapons ] restriction_blast_protection: - enchantments: [blast_protection] - notAffectedGroups: [enchanted_book, armors] + enchantments: [ minecraft:blast_protection ] + notAffectedGroups: [ enchanted_book, armors ] restriction_channeling: - enchantments: [channeling] - notAffectedGroups: [enchanted_book, trident] + enchantments: [ minecraft:channeling ] + notAffectedGroups: [ enchanted_book, trident ] restriction_binding_curse: - enchantments: [binding_curse] - notAffectedGroups: [enchanted_book, wearable] + enchantments: [ minecraft:binding_curse ] + notAffectedGroups: [ enchanted_book, wearable ] restriction_vanishing_curse: - enchantments: [vanishing_curse] - notAffectedGroups: [enchanted_book, can_vanish] + enchantments: [ minecraft:vanishing_curse ] + notAffectedGroups: [ enchanted_book, can_vanish ] restriction_depth_strider: - enchantments: [depth_strider] - notAffectedGroups: [enchanted_book, boots] + enchantments: [ minecraft:depth_strider ] + notAffectedGroups: [ enchanted_book, boots ] restriction_efficiency: - enchantments: [efficiency] - notAffectedGroups: [enchanted_book, tools, shears] + enchantments: [ minecraft:efficiency ] + notAffectedGroups: [ enchanted_book, tools, shears ] restriction_feather_falling: - enchantments: [feather_falling] - notAffectedGroups: [enchanted_book, boots] + enchantments: [ minecraft:feather_falling ] + notAffectedGroups: [ enchanted_book, boots ] restriction_fire_aspect: - enchantments: [fire_aspect] - notAffectedGroups: [enchanted_book, swords] + enchantments: [ minecraft:fire_aspect ] + notAffectedGroups: [ enchanted_book, swords ] restriction_fire_protection: - enchantments: [fire_protection] - notAffectedGroups: [enchanted_book, armors] + enchantments: [ minecraft:fire_protection ] + notAffectedGroups: [ enchanted_book, armors ] restriction_flame: - enchantments: [flame] - notAffectedGroups: [enchanted_book, bow] + enchantments: [ minecraft:flame ] + notAffectedGroups: [ enchanted_book, bow ] restriction_fortune: - enchantments: [fortune] - notAffectedGroups: [enchanted_book, tools] + enchantments: [ minecraft:fortune ] + notAffectedGroups: [ enchanted_book, tools ] restriction_frost_walker: - enchantments: [frost_walker] - notAffectedGroups: [enchanted_book, boots] + enchantments: [ minecraft:frost_walker ] + notAffectedGroups: [ enchanted_book, boots ] restriction_impaling: - enchantments: [impaling] - notAffectedGroups: [enchanted_book, trident] + enchantments: [ minecraft:impaling ] + notAffectedGroups: [ enchanted_book, trident ] restriction_infinity: - enchantments: [infinity] - notAffectedGroups: [enchanted_book, bow] + enchantments: [ minecraft:infinity ] + notAffectedGroups: [ enchanted_book, bow ] restriction_knockback: - enchantments: [knockback] - notAffectedGroups: [enchanted_book, swords] + enchantments: [ minecraft:knockback ] + notAffectedGroups: [ enchanted_book, swords ] restriction_looting: - enchantments: [looting] - notAffectedGroups: [enchanted_book, swords] + enchantments: [ minecraft:looting ] + notAffectedGroups: [ enchanted_book, swords ] restriction_loyalty: - enchantments: [loyalty] - notAffectedGroups: [enchanted_book, trident] + enchantments: [ minecraft:loyalty ] + notAffectedGroups: [ enchanted_book, trident ] + +restriction_luck_of_the_sea: + enchantments: [ minecraft:luck_of_the_sea ] + notAffectedGroups: [ enchanted_book, fishing_rod ] restriction_lure: - enchantments: [lure] - notAffectedGroups: [enchanted_book, fishing_rod] + enchantments: [ minecraft:lure ] + notAffectedGroups: [ enchanted_book, fishing_rod ] restriction_mending: - enchantments: [mending] - notAffectedGroups: [enchanted_book, can_unbreak] + enchantments: [ minecraft:mending ] + notAffectedGroups: [ enchanted_book, can_unbreak ] -restriction_multishot: - enchantments: [multishot] - notAffectedGroups: [enchanted_book, crossbow] +restriction_minecraft_multishot: + enchantments: [ minecraft:multishot ] + notAffectedGroups: [ enchanted_book, crossbow ] restriction_piercing: - enchantments: [piercing] - notAffectedGroups: [enchanted_book, crossbow] + enchantments: [ minecraft:piercing ] + notAffectedGroups: [ enchanted_book, crossbow ] restriction_power: - enchantments: [power] - notAffectedGroups: [enchanted_book, bow] + enchantments: [ minecraft:power ] + notAffectedGroups: [ enchanted_book, bow ] restriction_projectile_protection: - enchantments: [projectile_protection] - notAffectedGroups: [enchanted_book, armors] + enchantments: [ minecraft:projectile_protection ] + notAffectedGroups: [ enchanted_book, armors ] restriction_protection: - enchantments: [protection] - notAffectedGroups: [enchanted_book, armors] + enchantments: [ minecraft:protection ] + notAffectedGroups: [ enchanted_book, armors ] restriction_punch: - enchantments: [punch] - notAffectedGroups: [enchanted_book, bow] + enchantments: [ minecraft:punch ] + notAffectedGroups: [ enchanted_book, bow ] restriction_quick_charge: - enchantments: [quick_charge] - notAffectedGroups: [enchanted_book, crossbow] + enchantments: [ minecraft:quick_charge ] + notAffectedGroups: [ enchanted_book, crossbow ] restriction_respiration: - enchantments: [respiration] - notAffectedGroups: [enchanted_book, helmets] + enchantments: [ minecraft:respiration ] + notAffectedGroups: [ enchanted_book, helmets ] restriction_riptide: - enchantments: [riptide] - notAffectedGroups: [enchanted_book, trident] + enchantments: [ minecraft:riptide ] + notAffectedGroups: [ enchanted_book, trident ] restriction_sharpness: - enchantments: [sharpness] - notAffectedGroups: [enchanted_book, melee_weapons] + enchantments: [ minecraft:sharpness ] + notAffectedGroups: [ enchanted_book, melee_weapons ] -restriction_silk_touch: - enchantments: [silk_touch] - notAffectedGroups: [enchanted_book, tools] +restriction__silk_touch: + enchantments: [ minecraft:silk_touch ] + notAffectedGroups: [ enchanted_book, tools ] restriction_smite: - enchantments: [smite] - notAffectedGroups: [enchanted_book, melee_weapons] + enchantments: [ minecraft:smite ] + notAffectedGroups: [ enchanted_book, melee_weapons ] restriction_soul_speed: - enchantments: [soul_speed] - notAffectedGroups: [enchanted_book, boots] + enchantments: [ minecraft:soul_speed ] + notAffectedGroups: [ enchanted_book, boots ] -restriction_sweeping: - enchantments: [sweeping] - notAffectedGroups: [enchanted_book, swords] +restriction_sweeping_edge: + enchantments: [ minecraft:sweeping, minecraft:sweeping_edge ] + notAffectedGroups: [ enchanted_book, swords ] # Do not exist in 1.18, that mean useInFuture will be set to true # useInFuture set to true also mean it will not warn if there is an issue restriction_swift_sneak: useInFuture: true - enchantments: [swift_sneak] - notAffectedGroups: [enchanted_book, boots] + enchantments: [ minecraft:swift_sneak ] + notAffectedGroups: [ enchanted_book, leggings ] restriction_thorns: - enchantments: [thorns] - notAffectedGroups: [enchanted_book, armors] + enchantments: [ minecraft:thorns ] + notAffectedGroups: [ enchanted_book, armors ] -restriction_unbreaking: - enchantments: [unbreaking] - notAffectedGroups: [enchanted_book, can_unbreak] +restriction__unbreaking: + enchantments: [ minecraft:unbreaking ] + notAffectedGroups: [ enchanted_book, can_unbreak ] # ---------------------------------------------------- # Now we have conflicts about enchantment Incompatibility @@ -175,60 +184,64 @@ restriction_unbreaking: sword_enchant_conflict: enchantments: - - bane_of_arthropods - - smite - - sharpness - notAffectedGroups: [] + - minecraft:bane_of_arthropods + - minecraft:smite + - minecraft:sharpness + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 protection_enchant_conflict: enchantments: - - blast_protection - - fire_protection - - projectile_protection - - protection - notAffectedGroups: [] + - minecraft:blast_protection + - minecraft:fire_protection + - minecraft:projectile_protection + - minecraft:protection + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 trident_conflict1: enchantments: - - channeling - - riptide - notAffectedGroups: [] + - minecraft:channeling + - minecraft:riptide + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 trident_conflict2: enchantments: - - loyalty - - riptide - notAffectedGroups: [] + - minecraft:loyalty + - minecraft:riptide + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 boot_conflict: enchantments: - - depth_strider - - frost_walker - notAffectedGroups: [] + - minecraft:depth_strider + - minecraft:frost_walker + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 tool_conflict: enchantments: - - fortune - - silk_touch - notAffectedGroups: [] + - minecraft:fortune + - minecraft:silk_touch + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 bow_conflict: enchantments: - - mending - - infinity - notAffectedGroups: [] + - minecraft:mending + - minecraft:infinity + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 crossbow_conflict: enchantments: - - multishot - - piercing - notAffectedGroups: [] + - minecraft:multishot + - minecraft:piercing + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 +# ---------------------------------------------------- +# Bellow is for custom conflicts. +# This is also where conflict create from the gui will be placed. +# ---------------------------------------------------- diff --git a/src/main/resources/item_groups.yml b/src/main/resources/item_groups.yml index 2e222aa..8a62f3d 100644 --- a/src/main/resources/item_groups.yml +++ b/src/main/resources/item_groups.yml @@ -1,3 +1,8 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + # Please note this config use spigot material names. # It should match minecraft name in most case, maybe every case, but I can't be sure # In case there an issue with material name, you can found them here: @@ -121,7 +126,7 @@ wearable: groups: - armors -tools: +pickaxes: type: include items: - wooden_pickaxe @@ -130,19 +135,33 @@ tools: - diamond_pickaxe - golden_pickaxe - netherite_pickaxe + +shovels: + type: include + items: - wooden_shovel - stone_shovel - iron_shovel - diamond_shovel - golden_shovel - netherite_shovel + +hoes: + type: include + items: - wooden_hoe - stone_hoe - iron_hoe - diamond_hoe - golden_hoe - netherite_hoe + +tools: + type: include groups: + - pickaxes + - shovels + - hoes - axes enchanted_book: diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index b3adc0b..dab9997 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,20 +1,83 @@ -main: io.delilaheve.UnsafeEnchants -name: UnsafeEnchantsPlus -prefix: UnsafeEnchants+ -version: 1.1.5 -description: Allow custom illegal enchantment -api-version: 1.18 +main: io.delilaheve.CustomAnvil +name: CustomAnvil +prefix: "Custom Anvil" +version: ${version} +folia-supported: true +description: Allow to customise anvil mechanics +api-version: 1.16 load: POSTWORLD -authors: [DelilahEve, alexcrea] -libraries: - - org.jetbrains.kotlin:kotlin-stdlib:1.6.21 +authors: [ DelilahEve, alexcrea ] +libraries: [${libraries}] + +commands: + customanvil: + description: Generic command for custom anvil + aliases: + - ca + anvilconfigreload: + description: Reload every config of this plugin + permission: ca.command.reload + aliases: + #- acreload # anvil config reload + #- careload # custom anvil reload + - carl # custom anvil reload + customanvilconfig: + description: open a menu for administrator to edit plugin's config in game + permission: ca.config.edit + aliases: + - configanvil + permissions: - ue.unsafe: + ca.affected: default: true - description: Allow player to combine allowed "unsafe" enchants - ue.bypass.fuse: + description: Player with this permission will be affected by the plugin + ca.bypass.fuse: default: false - description: Allow player to combine every "unsafe" enchants - ue.bypass.level: + description: Allow player to combine every enchantments to every item (no custom limit) + ca.bypass.level: default: false - description: Allow player to bypass max level limit \ No newline at end of file + description: Allow player to bypass every level limit (no custom limit) + ca.command.reload: + default: op + description: Allow administrator to reload the plugin's configs + ca.command.diagnostic: + default: op + description: Get debug information about the plugin and server + ca.config.edit: + default: op + description: Allow administrator to edit the plugin's config in game + # color permissions + ca.color.code: + default: op + description: Allow player to use color code if enabled (toggleable) + ca.color.hex: + default: op + description: Allow player to use hexadecimal color if enabled (toggleable) + ca.rename.minimessage: + default: op + description: Allow player to use minimessage formating on rename if enabled (toggleable) (only legacy compatible at the time) + # lore edit permissions + ca.lore_edit.book: + default: op + description: Allow player to edit lore via book and quil if enabled (toggleable) + ca.lore_edit.paper: + default: op + description: Allow player to edit lore via paper if enabled (toggleable) + # dialog menu permission + ca.rename.dialog: + default: op + description: Allow player to use the dialog rename menu (toggleable) + + +# soft depend on old name of this plugin (UnsafeEnchantsPlus), so I can disable it if it is on the same server +# Also depend to other plugin for compatibility +softdepend: + - UnsafeEnchantsPlus + - ProtocolLib + - Disenchantment + - EnchantsSquared + - EcoEnchants + - eco + - ExcellentEnchants + - HavenBags + - SuperEnchants diff --git a/src/main/resources/unit_repair_item.yml b/src/main/resources/unit_repair_item.yml new file mode 100644 index 0000000..2902cce --- /dev/null +++ b/src/main/resources/unit_repair_item.yml @@ -0,0 +1,190 @@ +# +# It is recommended that you use /configanvil to edit theses config. +# You can still manually edit here if you like to. but if you do, don't forget to /anvilconfigreload after you changes ! +# + +# Unit repair configuration +# +# This configuration is to make custom unit repair +# A unit repair is, for example, a diamond to repair a diamond sword +# In vanilla, a unit repair 25% of object durability +# you can make a custom value here +# +# Item name should NOT combine caps and no caps (example: Stone) + +# Default value if the config is an invalid value (value <= 0 ) +# If value > 1 it will be treated as being = 1 +default_repair_amount: 0.25 + +# You can add custom unit repair +# The example bellow make a shield repaired by 10% by sticks + +#stick: +# shield: 0.10 + + +# Vanilla unit repair group is bellow +diamond: + diamond_helmet: 0.25 + diamond_chestplate: 0.25 + diamond_leggings: 0.25 + diamond_boots: 0.25 + diamond_sword: 0.25 + diamond_pickaxe: 0.25 + diamond_axe: 0.25 + diamond_shovel: 0.25 + diamond_hoe: 0.25 + +netherite_ingot: + netherite_helmet: 0.25 + netherite_chestplate: 0.25 + netherite_leggings: 0.25 + netherite_boots: 0.25 + netherite_sword: 0.25 + netherite_pickaxe: 0.25 + netherite_axe: 0.25 + netherite_shovel: 0.25 + netherite_hoe: 0.25 + +gold_ingot: + golden_helmet: 0.25 + golden_chestplate: 0.25 + golden_leggings: 0.25 + golden_boots: 0.25 + golden_sword: 0.25 + golden_pickaxe: 0.25 + golden_axe: 0.25 + golden_shovel: 0.25 + golden_hoe: 0.25 + +iron_ingot: + iron_helmet: 0.25 + iron_chestplate: 0.25 + iron_leggings: 0.25 + iron_boots: 0.25 + iron_sword: 0.25 + iron_pickaxe: 0.25 + iron_axe: 0.25 + iron_shovel: 0.25 + iron_hoe: 0.25 + +cobblestone: + stone_sword: 0.25 + stone_pickaxe: 0.25 + stone_axe: 0.25 + stone_shovel: 0.25 + stone_hoe: 0.25 + +cobbled_deepslate: + stone_sword: 0.25 + stone_pickaxe: 0.25 + stone_axe: 0.25 + stone_shovel: 0.25 + stone_hoe: 0.25 + +blackstone: + stone_sword: 0.25 + stone_pickaxe: 0.25 + stone_axe: 0.25 + stone_shovel: 0.25 + stone_hoe: 0.25 + +leather: + leather_helmet: 0.25 + leather_chestplate: 0.25 + leather_leggings: 0.25 + leather_boots: 0.25 + +phantom_membrane: + elytra: 0.25 + +scute: + turtle_helmet: 0.25 + +oak_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +spruce_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +birch_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +jungle_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +acacia_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +dark_oak_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +mangrove_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +cherry_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +bamboo_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +crimson_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 + +warped_planks: + wooden_sword: 0.25 + wooden_pickaxe: 0.25 + wooden_axe: 0.25 + wooden_shovel: 0.25 + wooden_hoe: 0.25 + shield: 0.25 diff --git a/src/test/java/io/delilaheve/CustomAnvilTests.java b/src/test/java/io/delilaheve/CustomAnvilTests.java new file mode 100644 index 0000000..12e1ec1 --- /dev/null +++ b/src/test/java/io/delilaheve/CustomAnvilTests.java @@ -0,0 +1,22 @@ +package io.delilaheve; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import xyz.alexcrea.cuanvil.tests.DefaultCustomAnvilTest; + +public class CustomAnvilTests extends DefaultCustomAnvilTest { + + @Test + public void simpleInitTest() { + try { + Assertions.assertNotNull(server); + Assertions.assertNotNull(plugin); + + // Test shutdown + plugin.onDisable(); + } catch (Exception e) { + Assertions.fail(e); + } + } + +} diff --git a/src/test/java/io/delilaheve/util/EnchantmentUtilTests.java b/src/test/java/io/delilaheve/util/EnchantmentUtilTests.java new file mode 100644 index 0000000..10ea252 --- /dev/null +++ b/src/test/java/io/delilaheve/util/EnchantmentUtilTests.java @@ -0,0 +1,184 @@ +package io.delilaheve.util; + +import io.delilaheve.CustomAnvil; +import org.bukkit.Material; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.AnvilInventory; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.permissions.PermissionAttachment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; +import org.mockbukkit.mockbukkit.inventory.ItemStackMock; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.tests.ConfigResetCustomAnvilTest; +import xyz.alexcrea.cuanvil.data.AnvilFuseTestData; +import xyz.alexcrea.cuanvil.util.AnvilFuseTestUtil; + +import java.util.List; + +public class EnchantmentUtilTests extends ConfigResetCustomAnvilTest { + + private AnvilInventory anvil; + private PlayerMock player; + + @Override + @BeforeEach + public void setUp() { + super.setUp(); + // Mock used player & open anvil + player = server.addPlayer(); + + Inventory anvil = server.createInventory(player, InventoryType.ANVIL); + + this.anvil = (AnvilInventory) anvil; + player.openInventory(anvil); + + ConfigHolder.DEFAULT_CONFIG.getConfig().set("debug_log", true); + ConfigHolder.DEFAULT_CONFIG.getConfig().set("debug_log_verbose", true); + } + + @Test + public void testBypassFuse(){ + // Test permission did not changed (if it do then server owner should be warned) + String permission = CustomAnvil.bypassFusePermission; + Assertions.assertEquals("ca.bypass.fuse", permission, "bypass fuse permission changed. " + + "Caution with that as it will break some server CustomAnvil setup."); + + // Create ingredient item + ItemStack normalStick = new ItemStackMock(Material.STICK); + ItemStack sharpnessBook = AnvilFuseTestUtil.prepareItem( + Material.ENCHANTED_BOOK, + List.of("sharpness"), 1); + + ItemStack sharpnessStick = AnvilFuseTestUtil.prepareItem( + Material.STICK, + List.of("sharpness"), 1); + + // Create result item + ItemStack sharpnessResultStick = AnvilFuseTestUtil.prepareItem( + Material.STICK, 1, + List.of("sharpness"), 1); + ItemStack sharpness2ResultStick = AnvilFuseTestUtil.prepareItem( + Material.STICK, 1, + List.of("sharpness"), 2); + + // Create failing anvil fuse data + AnvilFuseTestData nullResultData = new AnvilFuseTestData( + normalStick, sharpnessBook, + null + ); + AnvilFuseTestData nullResultData2 = new AnvilFuseTestData( + sharpnessStick, sharpnessStick, + null + ); + + // Create successful anvil fuse data + AnvilFuseTestData legalResultData = new AnvilFuseTestData( + normalStick, sharpnessBook, + sharpnessResultStick + // TODO add expected price + ); + AnvilFuseTestData legalResultData2 = new AnvilFuseTestData( + sharpnessStick, sharpnessStick, + sharpness2ResultStick + // TODO add expected price + ); + + // Test with no permission + nullResultData.executeTest(anvil, player); + nullResultData2.executeTest(anvil, player); + + // Add permission + PermissionAttachment attachment = player.addAttachment(plugin); + attachment.setPermission(permission, true); + + // Test with new permission + legalResultData.executeTest(anvil, player); + legalResultData2.executeTest(anvil, player); + } + + @Test + public void testLeveLimitFuse(){ + String permission = CustomAnvil.bypassLevelPermission; + Assertions.assertEquals("ca.bypass.level", permission, "level fuse permission changed. " + + "Caution with that as it will break some server CustomAnvil setup."); + + // Create ingredient item + ItemStack sharpness5Sword = AnvilFuseTestUtil.prepareItem( + Material.DIAMOND_SWORD, + List.of("sharpness"), 5); + + ItemStack sharpnessBook = AnvilFuseTestUtil.prepareItem( + Material.ENCHANTED_BOOK, + List.of("sharpness"), 1); + ItemStack sharpness5Book = AnvilFuseTestUtil.prepareItem( + Material.ENCHANTED_BOOK, + List.of("sharpness"), 5); + ItemStack sharpness6Book = AnvilFuseTestUtil.prepareItem( + Material.ENCHANTED_BOOK, + List.of("sharpness"), 6); + + // Create result item + ItemStack sharpness2BookResult = AnvilFuseTestUtil.prepareItem( + Material.ENCHANTED_BOOK, 1, + List.of("sharpness"), 2); + ItemStack sharpness6SwordResult = AnvilFuseTestUtil.prepareItem( + Material.DIAMOND_SWORD, 1, + List.of("sharpness"), 6); + + // Create failing anvil fuse data + AnvilFuseTestData nullResultData = new AnvilFuseTestData( + sharpnessBook, sharpnessBook, + null + ); + AnvilFuseTestData nullResultData2 = new AnvilFuseTestData( + sharpness5Sword, sharpness6Book, + null + ); + AnvilFuseTestData nullResultData3 = new AnvilFuseTestData( + sharpness5Sword, sharpness5Book, + null + ); + + // Create successful anvil fuse data + AnvilFuseTestData legalResultData = new AnvilFuseTestData( + sharpnessBook, sharpnessBook, + sharpness2BookResult + // TODO add expected price + ); + AnvilFuseTestData legalResultData2 = new AnvilFuseTestData( + sharpness5Sword, sharpness6Book, + sharpness6SwordResult + // TODO add expected price + ); + AnvilFuseTestData legalResultData3 = new AnvilFuseTestData( + sharpness5Sword, sharpness5Book, + sharpness6SwordResult + // TODO add expected price + ); + + // Test failing result first + nullResultData2.executeTest(anvil, player); + nullResultData3.executeTest(anvil, player); + + // Test working sharpness 2 + legalResultData.executeTest(anvil, player); + + // Set merge limit to 2 & test + ConfigHolder.DEFAULT_CONFIG.getConfig().set("disable-merge-over.minecraft:sharpness", 1); + nullResultData.executeTest(anvil, player); + + // Add permission + PermissionAttachment attachment = player.addAttachment(plugin); + attachment.setPermission(permission, true); + + // Test working sharpness 2 + legalResultData.executeTest(anvil, player); + legalResultData2.executeTest(anvil, player); + legalResultData3.executeTest(anvil, player); + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/anvil/AnvilFuseTests.java b/src/test/java/xyz/alexcrea/cuanvil/anvil/AnvilFuseTests.java new file mode 100644 index 0000000..19f1d6f --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/anvil/AnvilFuseTests.java @@ -0,0 +1,126 @@ +package xyz.alexcrea.cuanvil.anvil; + +import io.delilaheve.util.ConfigOptions; +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.AnvilInventory; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.Repairable; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.tests.SharedCustomAnvilTest; +import xyz.alexcrea.cuanvil.data.AnvilFuseTestData; +import xyz.alexcrea.cuanvil.util.AnvilFuseTestUtil; +import xyz.alexcrea.cuanvil.util.CommonItemUtil; + +public class AnvilFuseTests extends SharedCustomAnvilTest { + + private static AnvilInventory anvil; + private static PlayerMock player; + + @BeforeAll + public static void setUp() { + // Mock used player & open anvil + player = server.addPlayer(); + + Inventory anvil = server.createInventory(player, InventoryType.ANVIL); + + AnvilFuseTests.anvil = (AnvilInventory) anvil; + player.openInventory(anvil); + + ConfigHolder.DEFAULT_CONFIG.getConfig().set(ConfigOptions.DEBUG_LOGGING, true); + ConfigHolder.DEFAULT_CONFIG.getConfig().set(ConfigOptions.VERBOSE_DEBUG_LOGGING, true); + ConfigHolder.DEFAULT_CONFIG.getConfig().set(ConfigOptions.ALLOW_COLOR_CODE, true); // For rename test + } + + @BeforeEach + public void prepareAnvil(){ + anvil.clear(); + } + + @AfterAll + public static void tearDown() { + player = null; + anvil = null; + } + + @Test + public void mergeFuseTest(){ + // Literally just test a sharpness 4 + sharpness 4 + ItemStack sharpness4 = CommonItemUtil.sharpness(4); + + ItemStack sharpness5Result = CommonItemUtil.sharpness(5); + Repairable meta = (Repairable) sharpness5Result.getItemMeta(); + meta.setRepairCost(1); + sharpness5Result.setItemMeta(meta); + + AnvilFuseTestData data = new AnvilFuseTestData( + sharpness4, sharpness4, + sharpness5Result, + 5 + ); + + data.executeTest(anvil, player); + } + + @Test + public void overFuseTest(){ + // Test sharpness 4 + sharpness 5 + ItemStack sharpness4 = CommonItemUtil.sharpness(4); + ItemStack sharpness5 = CommonItemUtil.sharpness(5); + + ItemStack sharpness5Result = CommonItemUtil.sharpness(5); + Repairable meta = (Repairable) sharpness5Result.getItemMeta(); + meta.setRepairCost(1); + sharpness5Result.setItemMeta(meta); + + AnvilFuseTestData data = new AnvilFuseTestData( + sharpness4, sharpness5, + sharpness5Result, + 5 + ); + + data.executeTest(anvil, player); + } + + @Test + public void underFuseTest(){ + // test sharpness 5 + 4. Custom Anvil should not allow it to be as it result as the same item as left item + ItemStack sharpness4 = CommonItemUtil.sharpness(4); + ItemStack sharpness5 = CommonItemUtil.sharpness(5); + + AnvilFuseTestData data = new AnvilFuseTestData( + sharpness5, sharpness4, + null + ); + + data.executeTest(anvil, player); + } + + // Note: currently anvil can only have null name. maybe handle differently later + @Test + public void nullNameResetTest(){ + ItemStack base = new ItemStack(Material.NETHERITE_SWORD); + ItemStack expected = base.clone(); + + ItemMeta meta = expected.getItemMeta(); + meta.displayName(Component.text("test")); + base.setItemMeta(meta); + + AnvilFuseTestData data = new AnvilFuseTestData( + base, null, + expected, expected, null, + 1, null, 1 + ); + + data.executeTest(anvil, player); + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/anvil/LoreEditTests.java b/src/test/java/xyz/alexcrea/cuanvil/anvil/LoreEditTests.java new file mode 100644 index 0000000..668da58 --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/anvil/LoreEditTests.java @@ -0,0 +1,622 @@ +package xyz.alexcrea.cuanvil.anvil; + +import io.delilaheve.util.ConfigOptions; +import org.bukkit.Material; +import org.bukkit.event.Event; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.AnvilInventory; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BookMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockbukkit.mockbukkit.entity.PlayerMock; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.data.AnvilClickTestData; +import xyz.alexcrea.cuanvil.data.AnvilFuseTestData; +import xyz.alexcrea.cuanvil.data.TestDataContainer; +import xyz.alexcrea.cuanvil.tests.SharedCustomAnvilTest; +import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil; +import xyz.alexcrea.cuanvil.util.config.LoreEditType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +// TODO redo test as now color split should be handled in AnvilColorUtilTest and not here +// Especially since some behavior changed +public class LoreEditTests extends SharedCustomAnvilTest { + + /*private static AnvilInventory anvil; + private static PlayerMock player; + + private static final String COLORED_LORE_LINE = "§x§1§2§3§4§5§6TEST §atest"; + private static final String UNCOLORED_LORE_LINE = "#123456TEST &atest"; + + private static final int COLOR_USE_COST = 1; + private static final int COLOR_REMOVE_COST = 2; + private static final int NON_ONE_TEST_FIXED_COST = 10; + private static final int NON_ZERO_TEST_LINE_COST = 2; + + private static ItemStack emptyItem; + private static ItemStack oneColoredLoreItem; + private static ItemStack twoColoredLoreItem; + private static ItemStack oneUncoloredLoreItem; + private static ItemStack twoUncoloredLoreItem; + + private static ItemStack emptyPaperStack; + private static ItemStack emptyPaper63Item; + private static ItemStack emptyPaperOne; + private static ItemStack coloredPaperStack; + private static ItemStack coloredPaper63Item; + private static ItemStack coloredPaperOne; + private static ItemStack uncoloredPaperStack; + private static ItemStack uncoloredPaper63Item; + private static ItemStack uncoloredPaperOne; + + private static ItemStack emptyBook; + private static ItemStack coloredBook1Line; + private static ItemStack coloredBook2Line; + private static ItemStack uncoloredBook1Line; + private static ItemStack uncoloredBook2Line; + + private static TestDataContainer defBookAppend; + private static TestDataContainer defBookRemove; + + private static TestDataContainer defPaperAppend; + private static TestDataContainer defPaperRemove; + + private static TestDataContainer defMultilineBookAppend; + private static TestDataContainer defMultilineBookRemove; + + private static TestDataContainer defMultilinePaperAppend; + private static TestDataContainer defMultilinePaperRemove; + + private static Map singleLineTypeToTest; + private static Map multiLineTypeToTest; + + @BeforeAll + public static void setUp() { + // Mock used player & open anvil + player = server.addPlayer(); + + Inventory anvil = server.createInventory(player, InventoryType.ANVIL); + + LoreEditTests.anvil = (AnvilInventory) anvil; + player.openInventory(anvil); + + ConfigHolder.DEFAULT_CONFIG.getConfig().set(ConfigOptions.DEBUG_LOGGING, true); + ConfigHolder.DEFAULT_CONFIG.getConfig().set(ConfigOptions.VERBOSE_DEBUG_LOGGING, true); + + // Applied item + ItemStack item = new ItemStack(Material.STICK, 33); + ItemMeta meta = item.getItemMeta(); + ArrayList lore = new ArrayList<>(); + + emptyItem = item.clone(); + + lore.add(COLORED_LORE_LINE); + meta.setLore(lore); + item.setItemMeta(meta); + oneColoredLoreItem = item.clone(); + + lore.add(COLORED_LORE_LINE); + meta.setLore(lore); + item.setItemMeta(meta); + twoColoredLoreItem = item.clone(); + + lore.clear(); + lore.add(UNCOLORED_LORE_LINE); + meta.setLore(lore); + item.setItemMeta(meta); + oneUncoloredLoreItem = item.clone(); + + lore.add(UNCOLORED_LORE_LINE); + meta.setLore(lore); + item.setItemMeta(meta); + twoUncoloredLoreItem = item.clone(); + lore.clear(); + + // Paper items + item = new ItemStack(Material.PAPER, 64); + meta = item.getItemMeta(); + emptyPaperStack = item.clone(); + item.setAmount(63); + emptyPaper63Item = item.clone(); + item.setAmount(1); + emptyPaperOne = item.clone(); + + item.setAmount(64); + meta.setDisplayName(COLORED_LORE_LINE); + item.setItemMeta(meta); + coloredPaperStack = item.clone(); + item.setAmount(63); + coloredPaper63Item = item.clone(); + item.setAmount(1); + coloredPaperOne = item.clone(); + + item.setAmount(64); + meta.setDisplayName(UNCOLORED_LORE_LINE); + item.setItemMeta(meta); + uncoloredPaperStack = item.clone(); + item.setAmount(63); + uncoloredPaper63Item = item.clone(); + item.setAmount(1); + uncoloredPaperOne = item.clone(); + + // Book items + item = new ItemStack(Material.WRITABLE_BOOK); + BookMeta bookmeta = (BookMeta) item.getItemMeta(); + emptyBook = item.clone(); + + bookmeta.setPages(COLORED_LORE_LINE); + item.setItemMeta(bookmeta); + coloredBook1Line = item.clone(); + + bookmeta.setPages(COLORED_LORE_LINE + "\n" + COLORED_LORE_LINE); + item.setItemMeta(bookmeta); + coloredBook2Line = item.clone(); + + bookmeta.setPages(UNCOLORED_LORE_LINE); + item.setItemMeta(bookmeta); + uncoloredBook1Line = item.clone(); + + bookmeta.setPages(UNCOLORED_LORE_LINE + "\n" + UNCOLORED_LORE_LINE); + item.setItemMeta(bookmeta); + uncoloredBook2Line = item.clone(); + + // Default working test data + defBookAppend = new TestDataContainer( + new AnvilFuseTestData( + emptyItem, uncoloredBook1Line, + oneColoredLoreItem, + 1 + ), new AnvilClickTestData( + null, emptyBook, null, oneColoredLoreItem, + 1, Event.Result.DENY, + true, Event.Result.DENY + )); + + defBookRemove = new TestDataContainer( + new AnvilFuseTestData( + oneColoredLoreItem, emptyBook, + emptyItem, + 1 + ), new AnvilClickTestData( + null, coloredBook1Line, null, emptyItem, + 1, Event.Result.DENY, + true, Event.Result.DENY + )); + + defPaperAppend = new TestDataContainer( + new AnvilFuseTestData( + emptyItem, uncoloredPaperStack, + oneColoredLoreItem, + 1 + ), new AnvilClickTestData( + emptyPaperOne, uncoloredPaper63Item, null, oneColoredLoreItem, + 1, Event.Result.DENY, + true, Event.Result.DENY + )); + + defPaperRemove = new TestDataContainer( + new AnvilFuseTestData( + oneColoredLoreItem, emptyPaperStack, + emptyItem, + 1 + ), new AnvilClickTestData( + coloredPaperOne, emptyPaper63Item, null, emptyItem, + 1, Event.Result.DENY, + true, Event.Result.DENY + )); + defMultilineBookAppend = new TestDataContainer( + new AnvilFuseTestData( + emptyItem, uncoloredBook2Line, + twoColoredLoreItem, + 1 + ), new AnvilClickTestData( + null, emptyBook, null, twoColoredLoreItem, + 1, Event.Result.DENY, + true, Event.Result.DENY + )); + + defMultilineBookRemove = new TestDataContainer( + new AnvilFuseTestData( + twoColoredLoreItem, emptyBook, + emptyItem, + 1 + ), new AnvilClickTestData( + null, coloredBook2Line, null, emptyItem, + 1, Event.Result.DENY, + true, Event.Result.DENY + )); + + defMultilinePaperAppend = new TestDataContainer( + new AnvilFuseTestData( + oneColoredLoreItem, uncoloredPaperStack, + twoColoredLoreItem, + 1 + ), new AnvilClickTestData( + emptyPaperOne, uncoloredPaper63Item, null, twoColoredLoreItem, + 1, Event.Result.DENY, + true, Event.Result.DENY + )); + + defMultilinePaperRemove = new TestDataContainer( + new AnvilFuseTestData( + twoColoredLoreItem, emptyPaperStack, + oneColoredLoreItem, + 1 + ), new AnvilClickTestData( + coloredPaperOne, emptyPaper63Item, null, oneColoredLoreItem, + 1, Event.Result.DENY, + true, Event.Result.DENY + )); + + singleLineTypeToTest = Map.of( + LoreEditType.APPEND_BOOK, defBookAppend, + LoreEditType.REMOVE_BOOK, defBookRemove, + LoreEditType.APPEND_PAPER, defPaperAppend, + LoreEditType.REMOVE_PAPER, defPaperRemove + ); + + multiLineTypeToTest = Map.of( + LoreEditType.APPEND_BOOK, defMultilineBookAppend, + LoreEditType.REMOVE_BOOK, defMultilineBookRemove, + LoreEditType.APPEND_PAPER, defMultilinePaperAppend, + LoreEditType.REMOVE_PAPER, defMultilinePaperRemove + ); + } + + @BeforeEach + public void prepareAnvil() { + anvil.clear(); + + // Make sure we reset value in case it got modified + for (@NotNull LoreEditType type : LoreEditType.values()) { + // Make sure it is enabled for the tests (unless its is enabled test) + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.IS_ENABLED, true); + + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.DO_CONSUME, false); + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.FIXED_COST, 1); + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.PER_LINE_COST, 0); + + // Make sur color is enabled by default + if (type.isAppend()) { + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.ALLOW_HEX_COLOR, true); + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.ALLOW_COLOR_CODE, true); + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.ALLOW_MINIMESSAGE, true); + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.USE_COLOR_COST, 0); + } else { + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.REMOVE_COLOR_COST, 0); + } + + } + + // Disable them by default and test them on specific tests + ConfigHolder.DEFAULT_CONFIG.getConfig().set(LoreEditConfigUtil.BOOK_PERMISSION_NEEDED, false); + ConfigHolder.DEFAULT_CONFIG.getConfig().set(LoreEditConfigUtil.PAPER_PERMISSION_NEEDED, false); + + ConfigHolder.DEFAULT_CONFIG.getConfig().set(LoreEditConfigUtil.PAPER_EDIT_ORDER, LoreEditConfigUtil.DEFAULT_PAPER_EDIT_ORDER); + } + + @AfterAll + public static void tearDown() { + player = null; + anvil = null; + } + + public @Nullable ItemStack uncoloredEquivalent(@Nullable ItemStack colored) { + // null check + if (null == colored) return null; + + if (oneColoredLoreItem == colored) return oneUncoloredLoreItem; + if (twoColoredLoreItem == colored) return twoUncoloredLoreItem; + + if (coloredPaperStack == colored) return uncoloredPaperStack; + if (coloredPaper63Item == colored) return uncoloredPaper63Item; + if (coloredPaperOne == colored) return uncoloredPaperOne; + + if (coloredBook1Line == colored) return uncoloredBook1Line; + if (coloredBook2Line == colored) return uncoloredBook2Line; + + // They already are uncolored + if (oneUncoloredLoreItem == colored) return oneUncoloredLoreItem; + if (twoUncoloredLoreItem == colored) return twoUncoloredLoreItem; + + if (uncoloredPaperStack == colored) return uncoloredPaperStack; + if (uncoloredPaper63Item == colored) return uncoloredPaper63Item; + if (uncoloredPaperOne == colored) return uncoloredPaperOne; + + if (uncoloredBook1Line == colored) return uncoloredBook1Line; + if (uncoloredBook2Line == colored) return uncoloredBook2Line; + + // No lore items return themself + if (emptyItem == colored) return emptyItem; + if (emptyBook == colored) return emptyBook; + if (emptyPaperStack == colored) return emptyPaperStack; + if (emptyPaper63Item == colored) return emptyPaper63Item; + if (emptyPaperOne == colored) return emptyPaperOne; + + Assertions.fail("Could not find uncolored version of " + colored); + return null; + } + + public static List onlyAppendTypes() { + ArrayList typeList = new ArrayList<>(); + for (@NotNull LoreEditType type : LoreEditType.values()) { + if (type.isAppend()) typeList.add(type); + } + return typeList; + } + + public static List onlyRemoveTypes() { + ArrayList typeList = new ArrayList<>(); + for (@NotNull LoreEditType type : LoreEditType.values()) { + if (!type.isAppend()) typeList.add(type); + } + return typeList; + } + + @ParameterizedTest + @EnumSource(LoreEditType.class) + public void simpleTest(LoreEditType type) { + // Test all defaults to make sure they works + singleLineTypeToTest.get(type).executeTest(anvil, player); + multiLineTypeToTest.get(type).executeTest(anvil, player); + + } + + @ParameterizedTest + @EnumSource(LoreEditType.class) + public void testPermissionNeeded_DEOP(LoreEditType type) { + ConfigHolder.DEFAULT_CONFIG.getConfig().set(LoreEditConfigUtil.BOOK_PERMISSION_NEEDED, true); + ConfigHolder.DEFAULT_CONFIG.getConfig().set(LoreEditConfigUtil.PAPER_PERMISSION_NEEDED, true); + player.setOp(false); + + singleLineTypeToTest.get(type).nullifyResult().executeTest(anvil, player); + multiLineTypeToTest.get(type).nullifyResult().executeTest(anvil, player); + } + + @ParameterizedTest + @EnumSource(LoreEditType.class) + public void testPermissionNeeded_OP(LoreEditType type) { + ConfigHolder.DEFAULT_CONFIG.getConfig().set(LoreEditConfigUtil.BOOK_PERMISSION_NEEDED, true); + ConfigHolder.DEFAULT_CONFIG.getConfig().set(LoreEditConfigUtil.PAPER_PERMISSION_NEEDED, true); + player.setOp(true); + + singleLineTypeToTest.get(type).executeTest(anvil, player); + multiLineTypeToTest.get(type).executeTest(anvil, player); + } + + @ParameterizedTest + @EnumSource(LoreEditType.class) + public void testLoreTypeDisabled(LoreEditType type) { + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.IS_ENABLED, false); + + singleLineTypeToTest.get(type).nullifyResult().executeTest(anvil, player); + multiLineTypeToTest.get(type).nullifyResult().executeTest(anvil, player); + } + + @ParameterizedTest + @EnumSource(LoreEditType.class) + public void testFixedCost_Default(LoreEditType type) { + singleLineTypeToTest.get(type).setCost(1).executeTest(anvil, player); + multiLineTypeToTest.get(type).setCost(1).executeTest(anvil, player); + } + + @ParameterizedTest + @EnumSource(LoreEditType.class) + public void testFixedCost_Modified(LoreEditType type) { + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.FIXED_COST, NON_ONE_TEST_FIXED_COST); + + singleLineTypeToTest.get(type).setCost(NON_ONE_TEST_FIXED_COST).executeTest(anvil, player); + multiLineTypeToTest.get(type).setCost(NON_ONE_TEST_FIXED_COST).executeTest(anvil, player); + } + + @ParameterizedTest + @EnumSource(LoreEditType.class) + public void testLineCost_Modified(LoreEditType type) { + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.PER_LINE_COST, NON_ZERO_TEST_LINE_COST); + + if (type.isMultiLine()) { + singleLineTypeToTest.get(type).setCost(NON_ZERO_TEST_LINE_COST + LoreEditConfigUtil.DEFAULT_FIXED_COST).executeTest(anvil, player); + multiLineTypeToTest.get(type).setCost(2 * NON_ZERO_TEST_LINE_COST + LoreEditConfigUtil.DEFAULT_FIXED_COST).executeTest(anvil, player); + } else { + singleLineTypeToTest.get(type).executeTest(anvil, player); + multiLineTypeToTest.get(type).executeTest(anvil, player); + } + } + + @ParameterizedTest + @EnumSource(LoreEditType.class) + public void testColorCost(LoreEditType type) { + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.USE_COLOR_COST, COLOR_USE_COST); + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.REMOVE_COLOR_COST, COLOR_REMOVE_COST); + + TestDataContainer singleLData = singleLineTypeToTest.get(type); + TestDataContainer multiLData = multiLineTypeToTest.get(type); + + if (type.isAppend()) { + singleLData.setCost(COLOR_USE_COST + LoreEditConfigUtil.DEFAULT_FIXED_COST).executeTest(anvil, player); + multiLData.setCost(COLOR_USE_COST + LoreEditConfigUtil.DEFAULT_FIXED_COST).executeTest(anvil, player); + } else { + singleLData + .setCost(COLOR_REMOVE_COST + LoreEditConfigUtil.DEFAULT_FIXED_COST) + .setClickRight(uncoloredEquivalent(singleLData.getRightClick())) + .setClickLeft(uncoloredEquivalent(singleLData.getLeftClick())) + .executeTest(anvil, player); + + multiLData.setCost(COLOR_REMOVE_COST + LoreEditConfigUtil.DEFAULT_FIXED_COST) + .setClickRight(uncoloredEquivalent(multiLData.getRightClick())) + .setClickLeft(uncoloredEquivalent(multiLData.getLeftClick())) + .executeTest(anvil, player); + } + } + + @ParameterizedTest + @MethodSource("onlyAppendTypes") + public void testColorDisabled(LoreEditType type) { + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.USE_COLOR_COST, COLOR_USE_COST); + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.ALLOW_HEX_COLOR, false); + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.ALLOW_COLOR_CODE, false); + + TestDataContainer singleLData = singleLineTypeToTest.get(type); + TestDataContainer multiLData = multiLineTypeToTest.get(type); + + singleLData + .setExpectedResult(uncoloredEquivalent(singleLData.getExpectedFuse())) + .executeTest(anvil, player); + multiLData + .setFuseLeft(uncoloredEquivalent(multiLData.getLeftFuse())) + .setExpectedResult(uncoloredEquivalent(multiLData.getExpectedFuse())) + .executeTest(anvil, player); + } + + @ParameterizedTest + @MethodSource("onlyRemoveTypes") + public void testColorRemoveEnabled(LoreEditType type) { + TestDataContainer singleLData = singleLineTypeToTest.get(type); + TestDataContainer multiLData = multiLineTypeToTest.get(type); + + singleLData + .setClickRight(uncoloredEquivalent(singleLData.getRightClick())) + .setClickLeft(uncoloredEquivalent(singleLData.getLeftClick())) + .executeTest(anvil, player); + multiLData + .setClickRight(uncoloredEquivalent(multiLData.getRightClick())) + .setClickLeft(uncoloredEquivalent(multiLData.getLeftClick())) + .executeTest(anvil, player); + } + + @ParameterizedTest + @MethodSource("onlyRemoveTypes") + public void testDoConsume(LoreEditType type) { + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.DO_CONSUME, true); + + TestDataContainer singleLData = singleLineTypeToTest.get(type); + TestDataContainer multiLData = multiLineTypeToTest.get(type); + + // NOTE: we set to null right item only on multi line bc the default data has more than one paper and would go to the left instead + singleLData = singleLData + .setClickRight(type.isMultiLine() ? null : singleLData.getRightClick()) + .setClickLeft(null); + singleLData.executeTest(anvil, player); + multiLData = multiLData + .setClickRight(type.isMultiLine() ? null : singleLData.getRightClick()) + .setClickLeft(null); + multiLData.executeTest(anvil, player); + + // Single paper consumed + if (!type.isMultiLine()) { + singleLData.setFuseRight(emptyPaperOne).setClickRight(null).executeTest(anvil, player); + multiLData.setFuseRight(emptyPaperOne).setClickRight(null).executeTest(anvil, player); + } + } + + private void SinglePaperTestPart(LoreEditType type, TestDataContainer data, ItemStack expectedFuse, ItemStack postFuse) { + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.DO_CONSUME, false); + data = data.setFuseRight(expectedFuse).setClickLeft(null).setClickRight(postFuse); + data.executeTest(anvil, player); + + ConfigHolder.DEFAULT_CONFIG.getConfig().set(type.getRootPath() + "." + LoreEditConfigUtil.DO_CONSUME, true); + data.setClickRight(null).executeTest(anvil, player); + } + + @Test + public void testSinglePaper_Append() { + SinglePaperTestPart(LoreEditType.APPEND_PAPER, + singleLineTypeToTest.get(LoreEditType.APPEND_PAPER), + uncoloredPaperOne, emptyPaperOne); + SinglePaperTestPart(LoreEditType.APPEND_PAPER, + multiLineTypeToTest.get(LoreEditType.APPEND_PAPER), + uncoloredPaperOne, emptyPaperOne); + } + + @Test + public void testSinglePaper_Remove() { + SinglePaperTestPart(LoreEditType.REMOVE_PAPER, + singleLineTypeToTest.get(LoreEditType.REMOVE_PAPER), + emptyPaperOne, coloredPaperOne); + SinglePaperTestPart(LoreEditType.REMOVE_PAPER, + multiLineTypeToTest.get(LoreEditType.REMOVE_PAPER), + emptyPaperOne, coloredPaperOne); + } + + @NotNull + private static ItemStack insertToLore(@NotNull ItemStack item, int index, @NotNull String toAppend) { + item = item.clone(); + ItemMeta meta = item.getItemMeta(); + Assertions.assertNotNull(meta); + Assertions.assertTrue(meta.hasLore()); + + ArrayList lore = new ArrayList<>(meta.getLore()); + lore.add(index, toAppend); + + meta.setLore(lore); + item.setItemMeta(meta); + return item; + } + + @NotNull + private static ItemStack setDisplayedName(@NotNull ItemStack item, @NotNull String name) { + item = item.clone(); + ItemMeta meta = item.getItemMeta(); + Assertions.assertNotNull(meta); + + meta.setDisplayName(name); + item.setItemMeta(meta); + return item; + } + + private static final String TESTED_LORE = "tested_lore"; + + @ParameterizedTest + @ValueSource(strings = {"sTaRt", "eNd"}) + public void testPaperOrder_Append(String order) { + ConfigHolder.DEFAULT_CONFIG.getConfig().set(LoreEditConfigUtil.PAPER_EDIT_ORDER, order); + + ItemStack result = insertToLore(oneColoredLoreItem, "start".equalsIgnoreCase(order) ? 0 : 1, TESTED_LORE); + ItemStack paper = setDisplayedName(emptyPaperOne, TESTED_LORE); + + new TestDataContainer( + new AnvilFuseTestData( + oneColoredLoreItem, paper, result, 1 + ), + new AnvilClickTestData( + null, emptyPaperOne, null, result, + 1, + Event.Result.DENY, true, Event.Result.DENY + ) + ).executeTest(anvil, player); + } + + @ParameterizedTest + @ValueSource(strings = {"sTaRt", "eNd"}) + public void testPaperOrder_Remove(String order) { + ConfigHolder.DEFAULT_CONFIG.getConfig().set(LoreEditConfigUtil.PAPER_EDIT_ORDER, order); + + ItemStack from = insertToLore(oneColoredLoreItem, "start".equalsIgnoreCase(order) ? 0 : 1, TESTED_LORE); + ItemStack paper = setDisplayedName(emptyPaperOne, TESTED_LORE); + + new TestDataContainer( + new AnvilFuseTestData( + from, emptyPaperOne, oneColoredLoreItem, 1 + ), + new AnvilClickTestData( + null, paper, null, oneColoredLoreItem, + 1, + Event.Result.DENY, true, Event.Result.DENY + ) + ).executeTest(anvil, player); + } + + //TODO work penalty test*/ + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/api/AnvilRecipeBuilderTest.java b/src/test/java/xyz/alexcrea/cuanvil/api/AnvilRecipeBuilderTest.java new file mode 100644 index 0000000..d9ff329 --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/api/AnvilRecipeBuilderTest.java @@ -0,0 +1,107 @@ +package xyz.alexcrea.cuanvil.api; + + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import xyz.alexcrea.cuanvil.tests.SharedOnlyMockBukkit; + +import static org.junit.jupiter.api.Assertions.*; + +public class AnvilRecipeBuilderTest extends SharedOnlyMockBukkit { + + private AnvilRecipeBuilder builder; + private AnvilRecipeBuilder builder2; + + @BeforeEach + public void setup() { + builder = new AnvilRecipeBuilder("test"); + builder2 = new AnvilRecipeBuilder("test"); + + builder2.setLeftItem(new ItemStack(Material.STICK)); + builder2.setResultItem(new ItemStack(Material.STICK)); + } + + @Test + void createBuilder_NoLeftItem(){ + builder.setResultItem(new ItemStack(Material.STICK)); + + assertNull(builder.build()); + } + + @Test + void createBuilder_NoResultItem(){ + builder.setLeftItem(new ItemStack(Material.STICK)); + + assertNull(builder.build()); + } + + @Test + void createBuilder_minimalist(){ + builder.setLeftItem(new ItemStack(Material.STICK)) + .setResultItem(new ItemStack(Material.STICK)); + + assertNotNull(builder.build()); + assertNotNull(builder2.build()); + } + + @Test + void setLeftItem(){ + assertNull(builder.getLeftItem()); + builder.setLeftItem(new ItemStack(Material.STICK)); + assertNotNull(builder.getLeftItem()); + } + + @Test + void setRightItem(){ + assertNull(builder.getRightItem()); + builder.setRightItem(new ItemStack(Material.STICK)); + assertNotNull(builder.getRightItem()); + } + + @Test + void setResultItem(){ + assertNull(builder.getResultItem()); + builder.setResultItem(new ItemStack(Material.STICK)); + assertNotNull(builder.getResultItem()); + } + + @Test + void setXpCostPerCraft(){ + assertEquals(0, builder2.getLevelCostPerCraft()); + assertEquals(0, builder2.build().getLevelCostPerCraft()); + builder2.setLevelCostPerCraft(2); + assertEquals(2, builder2.getLevelCostPerCraft()); + assertEquals(2, builder2.build().getLevelCostPerCraft()); + } + + @Test + void setLinearXpCostPerCraft(){ + assertEquals(0, builder2.getLinearXpCostPerCraft()); + assertEquals(0, builder2.build().getXpCostPerCraft()); + builder2.setLinearXpCostPerCraft(2); + assertEquals(2, builder2.getLinearXpCostPerCraft()); + assertEquals(2, builder2.build().getXpCostPerCraft()); + } + + + @Test + void setExactCount(){ + assertTrue(builder2.isExactCount()); + assertTrue(builder2.build().getExactCount()); + builder2.setExactCount(false); + assertFalse(builder2.isExactCount()); + assertFalse(builder2.build().getExactCount()); + } + + @Test + void setName(){ + assertEquals("test", builder2.getName()); + assertEquals("test", builder2.build().getName()); + builder2.setName("other"); + assertEquals("other", builder2.getName()); + assertEquals("other", builder2.build().getName()); + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/api/ConflictApiTests.java b/src/test/java/xyz/alexcrea/cuanvil/api/ConflictApiTests.java new file mode 100644 index 0000000..1a9a256 --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/api/ConflictApiTests.java @@ -0,0 +1,160 @@ +package xyz.alexcrea.cuanvil.api; + +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.AnvilInventory; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.group.EnchantConflictGroup; +import xyz.alexcrea.cuanvil.tests.ConfigResetCustomAnvilTest; +import xyz.alexcrea.cuanvil.data.AnvilFuseTestData; +import xyz.alexcrea.cuanvil.util.AnvilFuseTestUtil; +import xyz.alexcrea.cuanvil.util.CommonItemUtil; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ConflictApiTests extends ConfigResetCustomAnvilTest { + + private AnvilInventory anvil; + private PlayerMock player; + + @Override + @BeforeEach + public void setUp() { + super.setUp(); + // Mock used player & open anvil + player = server.addPlayer(); + + Inventory anvil = server.createInventory(player, InventoryType.ANVIL); + + this.anvil = (AnvilInventory) anvil; + player.openInventory(anvil); + + ConfigHolder.DEFAULT_CONFIG.getConfig().set("debug_log", true); + ConfigHolder.DEFAULT_CONFIG.getConfig().set("debug_log_verbose", true); + } + + @Test + public void testConflict() { + ItemStack sharpness1 = CommonItemUtil.sharpness(1); + ItemStack arthropods1 = CommonItemUtil.bane_of_arthropods(1); + ItemStack illegalResult = AnvilFuseTestUtil.prepareItem( + Material.DIAMOND_SWORD, 1, + List.of("bane_of_arthropods", "sharpness"), + 1, 1 + ); + + AnvilFuseTestData nullResultData = new AnvilFuseTestData( + sharpness1, arthropods1, + null + ); + + AnvilFuseTestData legalResultData = new AnvilFuseTestData( + sharpness1, arthropods1, + illegalResult, + 2 + ); + + CAEnchantment sharpness = EnchantmentApi.getByKey(Enchantment.SHARPNESS.getKey()); + Assertions.assertNotNull(sharpness); + + // Testing default conflict (illegal item should not be produced) + nullResultData.executeTest(anvil, player); + + // Try to find & remove conflict + EnchantConflictGroup conflict = findGroup("sword_enchant_conflict"); + Assertions.assertNotNull(conflict, "Could not find conflict."); + + // Test what happen when we remove the conflict (illegal item should be allowed) + ConflictAPI.removeConflict(conflict); + legalResultData.executeTest(anvil, player); + + // We create and add a new conflict + ConflictBuilder builder = new ConflictBuilder("sword_enchant_conflict"); + builder.addEnchantment("bane_of_arthropods").addEnchantment(sharpness); //TODO maybe add "add by bukkit enchantment" + builder.setMaxBeforeConflict(1); + + // Nothing should change as it is not new: it was previously deleted + Assertions.assertFalse(builder.registerIfNew()); + legalResultData.executeTest(anvil, player); + + // Now the conflict should be registered and conflict should exist + Assertions.assertTrue(builder.registerIfAbsent()); + nullResultData.executeTest(anvil, player); + } + + @Test + void writeGroup_Reload() { + String conflictName = "conflict"; + ConflictBuilder builder = new ConflictBuilder(conflictName); + builder.addEnchantment("bane_of_arthropods"); + + // Group not being set should not exist + assertFalse(doGroupExist(conflictName)); + + // Add group and reload + assertTrue(ConflictAPI.writeConflict(builder)); + assertFalse(doGroupExist(conflictName)); + + // Tick so write get reloaded + server.getScheduler().performOneTick(); + + assertTrue(doGroupExist(conflictName)); + } + + @Test + void writeGroup_Empty() { + String conflictName = "conflict"; + ConflictBuilder builder = new ConflictBuilder(conflictName); + + // Group not being set should not exist + assertFalse(doGroupExist(conflictName)); + + // Add group and reload + assertFalse(ConflictAPI.writeConflict(builder)); + assertFalse(doGroupExist(conflictName)); + + // Tick so write get reloaded + server.getScheduler().performOneTick(); + + assertFalse(doGroupExist(conflictName)); + } + + @Test + void writeGroup_InvalidDot() { + String conflictName = "conflict.conflict"; + ConflictBuilder builder = new ConflictBuilder(conflictName); + + // Try write group + assertFalse(ConflictAPI.writeConflict(builder)); + } + + // Maybe move to ConflictApi class ? + private static boolean doGroupExist(@NotNull String groupName) { + return findGroup(groupName) != null; + } + + // Maybe move to ConflictApi class ? + @Nullable + private static EnchantConflictGroup findGroup(@NotNull String groupName) { + for (EnchantConflictGroup enchantConflictGroup : ConflictAPI.getRegisteredConflict()) { + if (groupName.equalsIgnoreCase(enchantConflictGroup.getName())) { + return enchantConflictGroup; + } + } + return null; + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/api/ConflictBuilderTests.java b/src/test/java/xyz/alexcrea/cuanvil/api/ConflictBuilderTests.java new file mode 100644 index 0000000..670ae4e --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/api/ConflictBuilderTests.java @@ -0,0 +1,115 @@ +package xyz.alexcrea.cuanvil.api; + +import org.bukkit.NamespacedKey; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import xyz.alexcrea.cuanvil.group.IncludeGroup; +import xyz.alexcrea.cuanvil.tests.SharedOnlyMockBukkit; + +import static org.junit.jupiter.api.Assertions.*; + +public class ConflictBuilderTests extends SharedOnlyMockBukkit { + + private ConflictBuilder builder; + + @BeforeEach + public void setup() { + builder = new ConflictBuilder("test"); + } + + @Test + void testDefaults() { + assertNull(builder.getSource()); + assertEquals("an unknown source", builder.getSourceName()); + assertEquals("test", builder.getName()); + + assertTrue(builder.getEnchantmentNames().isEmpty()); + assertTrue(builder.getEnchantmentKeys().isEmpty()); + assertTrue(builder.getExcludedGroupNames().isEmpty()); + + assertEquals(0, builder.getMaxBeforeConflict()); + } + + @Test + void setName() { + assertEquals("test", builder.getName()); + assertEquals(builder, builder.setName("another")); + assertEquals("another", builder.getName()); + } + + @Test + void setMaxBeforeConflict() { + assertEquals(0, builder.getMaxBeforeConflict()); + assertEquals(builder, builder.setMaxBeforeConflict(1)); + assertEquals(1, builder.getMaxBeforeConflict()); + } + + @Test + void enchantmentString() { + assertTrue(builder.getEnchantmentNames().isEmpty()); + assertEquals(builder, builder.addEnchantment("bane_of_arthropods")); + assertEquals(1, builder.getEnchantmentNames().size()); + + assertEquals(builder, builder.removeEnchantment("bane_of_arthropods")); + assertTrue(builder.getEnchantmentNames().isEmpty()); + } + + @Test + void enchantmentKey() { + NamespacedKey key = NamespacedKey.fromString("bane_of_arthropods"); + assertNotNull(key); + + assertTrue(builder.getEnchantmentKeys().isEmpty()); + assertEquals(builder, builder.addEnchantment(key)); + assertEquals(1, builder.getEnchantmentKeys().size()); + + assertEquals(builder, builder.removeEnchantment(key)); + assertTrue(builder.getEnchantmentKeys().isEmpty()); + } + + @Test + void excludedGroup_String() { + assertTrue(builder.getExcludedGroupNames().isEmpty()); + assertEquals(builder, builder.addExcludedGroup("group")); + assertEquals(1, builder.getExcludedGroupNames().size()); + + assertEquals(builder, builder.removeExcludedGroup("group")); + assertTrue(builder.getExcludedGroupNames().isEmpty()); + } + + @Test + void excludedGroup_Group() { + IncludeGroup group = new IncludeGroup("group"); + + assertTrue(builder.getExcludedGroupNames().isEmpty()); + assertEquals(builder, builder.addExcludedGroup(group)); + assertEquals(1, builder.getExcludedGroupNames().size()); + + assertEquals(builder, builder.removeExcludedGroup(group)); + assertTrue(builder.getExcludedGroupNames().isEmpty()); + } + + @Test + void copy(){ + builder.setName("other"); + builder.setMaxBeforeConflict(1); + + builder.addEnchantment("bane_of_arthropods"); + builder.addEnchantment(NamespacedKey.fromString("bane_of_arthropods")); + builder.addExcludedGroup("group"); + builder.addExcludedGroup(new IncludeGroup("group2")); + + ConflictBuilder copy = builder.copy(); + assertEquals("other", copy.getName()); + assertEquals(1, copy.getMaxBeforeConflict()); + + assertEquals(1, copy.getEnchantmentNames().size()); + assertEquals("bane_of_arthropods", copy.getEnchantmentNames().stream().findFirst().orElse(null)); + + assertEquals(1, copy.getEnchantmentKeys().size()); + assertEquals(NamespacedKey.fromString("bane_of_arthropods"), copy.getEnchantmentKeys().stream().findFirst().orElse(null)); + + assertEquals(2, copy.getExcludedGroupNames().size()); + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/api/CustomAnvilRecipeApiTests.java b/src/test/java/xyz/alexcrea/cuanvil/api/CustomAnvilRecipeApiTests.java new file mode 100644 index 0000000..651b873 --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/api/CustomAnvilRecipeApiTests.java @@ -0,0 +1,259 @@ +package xyz.alexcrea.cuanvil.api; + +import org.bukkit.Material; +import org.bukkit.event.Event; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.AnvilInventory; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; +import org.mockbukkit.mockbukkit.inventory.ItemStackMock; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.data.AnvilClickTestData; +import xyz.alexcrea.cuanvil.data.TestDataContainer; +import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe; +import xyz.alexcrea.cuanvil.tests.ConfigResetCustomAnvilTest; +import xyz.alexcrea.cuanvil.data.AnvilFuseTestData; + +import static org.junit.jupiter.api.Assertions.*; + +public class CustomAnvilRecipeApiTests extends ConfigResetCustomAnvilTest { + + private AnvilInventory anvil; + private PlayerMock player; + + @Override + @BeforeEach + public void setUp() { + super.setUp(); + // Mock used player & open anvil + player = server.addPlayer(); + + Inventory anvil = server.createInventory(player, InventoryType.ANVIL); + + this.anvil = (AnvilInventory) anvil; + player.openInventory(anvil); + + ConfigHolder.DEFAULT_CONFIG.getConfig().set("debug_log", true); + ConfigHolder.DEFAULT_CONFIG.getConfig().set("debug_log_verbose", true); + } + + @Test + public void testBasicRecipe() { + String recipeName = "stick_recipe"; + ItemStack stick = new ItemStackMock(Material.STICK); + + AnvilFuseTestData nullResultData = new AnvilFuseTestData( + stick, stick, + null + ); + + AnvilFuseTestData legalResultData = new AnvilFuseTestData( + stick, stick, + null, stick, null, + 2, + null, null + ); + + // Testing default conflict (no recipe exist) + nullResultData.executeTest(anvil, player); + + // Add and test recipe + AnvilRecipeBuilder builder = new AnvilRecipeBuilder(recipeName); + builder.setExactCount(true).setLeftItem(stick).setResultItem(stick).setLevelCostPerCraft(2); + + assertTrue(builder.registerIfAbsent()); + legalResultData.executeTest(anvil, player); + + AnvilCustomRecipe recipe = getByName(recipeName); + assertNotNull(recipe); + + // Remove recipe + assertTrue(CustomAnvilRecipeApi.removeRecipe(recipe)); + assertFalse(CustomAnvilRecipeApi.removeRecipe(recipe)); + nullResultData.executeTest(anvil, player); + + recipe = getByName(recipeName); + assertNull(recipe); + + // Try to add deleted recipe with no override (should not add) + assertFalse(CustomAnvilRecipeApi.addRecipe(builder, false)); + nullResultData.executeTest(anvil, player); + + recipe = getByName(recipeName); + assertNull(recipe); + + // Try to add deleted recipe with override (should add) + assertTrue(CustomAnvilRecipeApi.addRecipe(builder, true)); + legalResultData.executeTest(anvil, player); + + recipe = getByName(recipeName); + assertNotNull(recipe); + } + + @Test + public void testUnitRecipe() { + String recipeName = "stick_recipe"; + ItemStack stick = new ItemStackMock(Material.STICK); + ItemStack stick2 = new ItemStackMock(Material.STICK, 2); + ItemStack stick5 = new ItemStackMock(Material.STICK, 5); + ItemStack stick10 = new ItemStackMock(Material.STICK, 10); + + AnvilFuseTestData nullResultData = new AnvilFuseTestData( + stick, stick, + null + ); + + AnvilFuseTestData legalResultData1 = new AnvilFuseTestData( + stick, stick, + null, stick2, null, + 2, + null, null + ); + + AnvilFuseTestData legalResultData2 = new AnvilFuseTestData( + stick5, stick, + null, stick10, null, + 10, // 2 * 5 + null, null + ); + + nullResultData.executeTest(anvil, player); + + AnvilRecipeBuilder builder = new AnvilRecipeBuilder(recipeName); + builder.setExactCount(false) + .setLeftItem(stick) + .setResultItem(stick2) + .setLevelCostPerCraft(2); + + assertTrue(builder.registerIfAbsent()); + + // Now working test + legalResultData1.executeTest(anvil, player); + legalResultData2.executeTest(anvil, player); + } + + @Test + public void testLinearXpCost() { + String recipeName = "stick_recipe"; + ItemStack stick = new ItemStackMock(Material.STICK); + ItemStack stick2 = new ItemStackMock(Material.STICK, 2); + ItemStack stick5 = new ItemStackMock(Material.STICK, 5); + ItemStack stick10 = new ItemStackMock(Material.STICK, 10); + + + AnvilFuseTestData nullResultData = new AnvilFuseTestData( + stick, stick, + null + ); + + TestDataContainer legalResultData1 = new TestDataContainer(new AnvilFuseTestData( + stick, stick, + null, stick2, null, + 1, + null, null + ), new AnvilClickTestData( + null, null, null, stick2, + 1, + Event.Result.DENY, true, Event.Result.DENY + + )); + + TestDataContainer legalResultData2 = new TestDataContainer(new AnvilFuseTestData( + stick5, stick, + null, stick10, null, + 4, + null, null + ), new AnvilClickTestData( + null, null, null, stick10, + 4, + Event.Result.DENY, true, Event.Result.DENY + + )); + + nullResultData.executeTest(anvil, player); + + AnvilRecipeBuilder builder = new AnvilRecipeBuilder(recipeName); + builder.setExactCount(false) + .setLeftItem(stick) + .setResultItem(stick2) + .setLevelCostPerCraft(0) + .setLinearXpCostPerCraft(10); + + assertTrue(builder.registerIfAbsent()); + + // Now working test + legalResultData1.executeTest(anvil, player); + legalResultData2.executeTest(anvil, player); + } + + @Test + public void testLinearXpCostRemoveExact() { + String recipeName = "stick_recipe"; + ItemStack stick = new ItemStackMock(Material.STICK); + ItemStack stick2 = new ItemStackMock(Material.STICK, 2); + ItemStack stick5 = new ItemStackMock(Material.STICK, 5); + ItemStack stick10 = new ItemStackMock(Material.STICK, 10); + + AnvilFuseTestData nullResultData = new AnvilFuseTestData( + stick, stick, + null + ); + + TestDataContainer legalResultData1 = new TestDataContainer(new AnvilFuseTestData( + stick, stick, + null, stick2, null, + 2, + null, null + ), new AnvilClickTestData( + null, null, null, stick2, + 2, + Event.Result.DENY, true, Event.Result.DENY + + )); + + TestDataContainer legalResultData2 = new TestDataContainer(new AnvilFuseTestData( + stick5, stick, + null, stick10, null, + 5, + null, null + ), new AnvilClickTestData( + null, null, null, stick10, + 5, + Event.Result.DENY, true, Event.Result.DENY + )); + + nullResultData.executeTest(anvil, player); + + AnvilRecipeBuilder builder = new AnvilRecipeBuilder(recipeName); + builder.setExactCount(false) + .setLeftItem(stick) + .setResultItem(stick2) + .setLinearXpCostPerCraft(10) + .setRemoveExactLinearXp(true); + + assertTrue(builder.registerIfAbsent()); + + // Now working test + legalResultData1.executeTest(anvil, player); + //TODO check exp ? + System.out.printf(String.valueOf(player.getExp())); + legalResultData2.executeTest(anvil, player); + //TODO check exp ? + } + + @Nullable + public static AnvilCustomRecipe getByName(String name) { + for (AnvilCustomRecipe registeredRecipe : CustomAnvilRecipeApi.getRegisteredRecipes()) { + if (registeredRecipe.getName().contentEquals(name)) { + return registeredRecipe; + } + } + + return null; + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/api/MaterialGroupApiTests.java b/src/test/java/xyz/alexcrea/cuanvil/api/MaterialGroupApiTests.java new file mode 100644 index 0000000..3fcafe7 --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/api/MaterialGroupApiTests.java @@ -0,0 +1,108 @@ +package xyz.alexcrea.cuanvil.api; + +import org.bukkit.Material; +import org.junit.jupiter.api.Test; +import xyz.alexcrea.cuanvil.group.EnchantConflictGroup; +import xyz.alexcrea.cuanvil.group.IncludeGroup; +import xyz.alexcrea.cuanvil.tests.ConfigResetCustomAnvilTest; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MaterialGroupApiTests extends ConfigResetCustomAnvilTest { + + @Test + void groupAddAndRemove() { + String groupName = "group"; + IncludeGroup group = new IncludeGroup(groupName); + group.addToPolicy(Material.DIAMOND_PICKAXE.getKey()); // We do not want it to be empty + + // Group not being set should not exist + assertFalse(doGroupExist(groupName)); + assertFalse(doGroupCanBeFound(groupName)); + + // Add group + assertTrue(MaterialGroupApi.addMaterialGroup(group)); + assertFalse(MaterialGroupApi.addMaterialGroup(group, true)); + + assertTrue(doGroupExist(groupName)); + assertTrue(doGroupCanBeFound(groupName)); + + // Remove group + assertTrue(MaterialGroupApi.removeGroup(group)); + assertFalse(MaterialGroupApi.removeGroup(group)); + + assertFalse(doGroupExist(groupName)); + assertFalse(doGroupCanBeFound(groupName)); + + // Re add + assertFalse(MaterialGroupApi.addMaterialGroup(group, false)); + assertTrue(MaterialGroupApi.addMaterialGroup(group, true)); + + assertTrue(doGroupExist(groupName)); + assertTrue(doGroupCanBeFound(groupName)); + + } + + @Test + void writeGroup_Reload() { + String groupName = "group"; + IncludeGroup group = new IncludeGroup(groupName); + group.addToPolicy(Material.DIAMOND_PICKAXE.getKey()); // We do not want it to be empty + + // Group not being set should not exist + assertFalse(doGroupExist(groupName)); + assertFalse(doGroupCanBeFound(groupName)); + + // Add group and reload + assertTrue(MaterialGroupApi.writeMaterialGroup(group)); + assertFalse(doGroupExist(groupName)); + assertFalse(doGroupCanBeFound(groupName)); + + // Tick so write get reloaded + server.getScheduler().performOneTick(); + + assertTrue(doGroupExist(groupName)); + assertTrue(doGroupCanBeFound(groupName)); + } + + @Test + void writeGroup_Empty() { + String groupName = "group"; + IncludeGroup group = new IncludeGroup(groupName); + + // Add group and reload + assertFalse(MaterialGroupApi.writeMaterialGroup(group)); + assertFalse(doGroupExist(groupName)); + assertFalse(doGroupCanBeFound(groupName)); + + // Tick so write get reloaded + server.getScheduler().performOneTick(); + + assertFalse(doGroupExist(groupName)); + assertFalse(doGroupCanBeFound(groupName)); + } + + @Test + void writeGroup_InvalidDot() { + String groupName = "group.group"; + IncludeGroup group = new IncludeGroup(groupName); + + // Try write group + assertFalse(MaterialGroupApi.writeMaterialGroup(group)); + } + + boolean doGroupExist(String groupName) { + return MaterialGroupApi.getGroup(groupName) != null; + } + + boolean doGroupCanBeFound(String groupName) { + ConflictBuilder builder = new ConflictBuilder(groupName); + builder.addExcludedGroup(groupName); + + EnchantConflictGroup group = builder.build(); + IncludeGroup materialGroup = (IncludeGroup) group.getCantConflictGroup(); + return materialGroup.getGroups().size() == 1; + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/api/UnitRepairApiTests.java b/src/test/java/xyz/alexcrea/cuanvil/api/UnitRepairApiTests.java new file mode 100644 index 0000000..adda4e1 --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/api/UnitRepairApiTests.java @@ -0,0 +1,118 @@ +package xyz.alexcrea.cuanvil.api; + +import org.bukkit.Material; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.AnvilInventory; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.Repairable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; +import org.mockbukkit.mockbukkit.inventory.ItemStackMock; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.tests.ConfigResetCustomAnvilTest; +import xyz.alexcrea.cuanvil.data.AnvilFuseTestData; +import xyz.alexcrea.cuanvil.util.AnvilFuseTestUtil; + +import static org.junit.jupiter.api.Assertions.*; + +public class UnitRepairApiTests extends ConfigResetCustomAnvilTest { + + private AnvilInventory anvil; + private PlayerMock player; + + @Override + @BeforeEach + public void setUp() { + super.setUp(); + // Mock used player & open anvil + player = server.addPlayer(); + + Inventory anvil = server.createInventory(player, InventoryType.ANVIL); + + this.anvil = (AnvilInventory) anvil; + player.openInventory(anvil); + + ConfigHolder.DEFAULT_CONFIG.getConfig().set("debug_log", true); + ConfigHolder.DEFAULT_CONFIG.getConfig().set("debug_log_verbose", true); + } + + @Test + void vanillaUnitRepair(){ + ItemStack damagedPickaxe = new ItemStackMock(Material.DIAMOND_PICKAXE); + damagedPickaxe.setDurability((short) (Material.DIAMOND_PICKAXE.getMaxDurability() -1)); + + ItemStack resultPickaxe = new ItemStackMock(Material.DIAMOND_PICKAXE); + resultPickaxe.setDurability((short) (Material.DIAMOND_PICKAXE.getMaxDurability()/2)); + ItemMeta meta = resultPickaxe.getItemMeta(); + ((Repairable) meta).setRepairCost(1); + resultPickaxe.setItemMeta(meta); + + ItemStack diamond2 = new ItemStackMock(Material.DIAMOND, 2); + + AnvilFuseTestData legalResultData = new AnvilFuseTestData( + damagedPickaxe, diamond2, + resultPickaxe, + 2 + ); + + legalResultData.executeTest(anvil, player); + } + + @Test + void removeUnitRepair(){ + ItemStack damagedPickaxe = new ItemStackMock(Material.DIAMOND_PICKAXE); + damagedPickaxe.setDurability((short) (Material.DIAMOND_PICKAXE.getMaxDurability() -1)); + + ItemStack diamond2 = new ItemStackMock(Material.DIAMOND, 2); + + AnvilFuseTestData nullResultData = new AnvilFuseTestData( + damagedPickaxe, diamond2, + null + ); + + // Remove unit repair + assertTrue(UnitRepairApi.removeUnitRepair(Material.DIAMOND, Material.DIAMOND_PICKAXE)); + + nullResultData.executeTest(anvil, player); + + // see override + assertFalse(UnitRepairApi.addUnitRepair(Material.DIAMOND, Material.DIAMOND_PICKAXE, 0.25)); + assertTrue(UnitRepairApi.addUnitRepair(Material.DIAMOND, Material.DIAMOND_PICKAXE, 0.25, true)); + } + + + @Test + void addUnitRepair(){ + ItemStack damagedPickaxe = new ItemStackMock(Material.DIAMOND_PICKAXE); + damagedPickaxe.setDurability((short) (Material.DIAMOND_PICKAXE.getMaxDurability() -1)); + + ItemStack resultPickaxe = new ItemStackMock(Material.DIAMOND_PICKAXE); + resultPickaxe.setDurability((short) (Material.DIAMOND_PICKAXE.getMaxDurability()/2)); + ItemMeta meta = resultPickaxe.getItemMeta(); + ((Repairable) meta).setRepairCost(1); + resultPickaxe.setItemMeta(meta); + + ItemStack stick2 = new ItemStackMock(Material.STICK, 2); + + AnvilFuseTestData nullResultData = new AnvilFuseTestData( + damagedPickaxe, stick2, + null + ); + AnvilFuseTestData legalResultData = new AnvilFuseTestData( + damagedPickaxe, stick2, + resultPickaxe, + 2 + ); + + nullResultData.executeTest(anvil, player); + + // Add unit repair + assertTrue(UnitRepairApi.addUnitRepair(Material.STICK, Material.DIAMOND_PICKAXE)); + assertFalse(UnitRepairApi.addUnitRepair(Material.STICK, Material.DIAMOND_PICKAXE)); + legalResultData.executeTest(anvil, player); + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/api/event/CAConfigReadyEventTest.java b/src/test/java/xyz/alexcrea/cuanvil/api/event/CAConfigReadyEventTest.java new file mode 100644 index 0000000..ff73a59 --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/api/event/CAConfigReadyEventTest.java @@ -0,0 +1,49 @@ +package xyz.alexcrea.cuanvil.api.event; + +import io.delilaheve.CustomAnvil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.matcher.plugin.PluginManagerFiredEventClassMatcher; +import xyz.alexcrea.cuanvil.tests.SharedOnlyMockBukkit; + +public class CAConfigReadyEventTest extends SharedOnlyMockBukkit { + + private CustomAnvil plugin; + + @Test + public void startup() { + boolean beforeStart = PluginManagerFiredEventClassMatcher + .hasNotFiredEventInstance(CAConfigReadyEvent.class) + .matches(server.getPluginManager()); + + Assertions.assertTrue(beforeStart, "Somehow, event fired before plugin being loaded ?"); + + // Load the plugin + plugin = MockBukkit.load(CustomAnvil.class); + + boolean postStart = PluginManagerFiredEventClassMatcher + .hasNotFiredEventInstance(CAConfigReadyEvent.class) + .matches(server.getPluginManager()); + + Assertions.assertTrue(postStart, "Event fired before plugin finished being loaded"); + + // Config load phase + server.getScheduler().performOneTick(); + boolean postConfig = PluginManagerFiredEventClassMatcher + .hasFiredEventInstance(CAConfigReadyEvent.class) + .matches(server.getPluginManager()); + + Assertions.assertTrue(postConfig, "Event did not fire after the config phase"); + } + + @AfterEach + public void pluginTeardown() { + if (plugin != null) { + server.getPluginManager().disablePlugin(plugin); + plugin = null; + } + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/api/event/CAEnchantRegistryReadyEventTest.java b/src/test/java/xyz/alexcrea/cuanvil/api/event/CAEnchantRegistryReadyEventTest.java new file mode 100644 index 0000000..2c79388 --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/api/event/CAEnchantRegistryReadyEventTest.java @@ -0,0 +1,49 @@ +package xyz.alexcrea.cuanvil.api.event; + +import io.delilaheve.CustomAnvil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.matcher.plugin.PluginManagerFiredEventClassMatcher; +import xyz.alexcrea.cuanvil.tests.SharedOnlyMockBukkit; + +public class CAEnchantRegistryReadyEventTest extends SharedOnlyMockBukkit { + + private CustomAnvil plugin; + + @Test + public void startup() { + boolean beforeStart = PluginManagerFiredEventClassMatcher + .hasNotFiredEventInstance(CAEnchantRegistryReadyEvent.class) + .matches(server.getPluginManager()); + + Assertions.assertTrue(beforeStart, "Somehow, event fired before plugin being loaded ?"); + + // Load the plugin + plugin = MockBukkit.load(CustomAnvil.class); + + boolean postStart = PluginManagerFiredEventClassMatcher + .hasNotFiredEventInstance(CAEnchantRegistryReadyEvent.class) + .matches(server.getPluginManager()); + + Assertions.assertTrue(postStart, "Event fired before plugin finished being loaded"); + + // Config load phase + server.getScheduler().performOneTick(); + boolean postConfig = PluginManagerFiredEventClassMatcher + .hasFiredEventInstance(CAEnchantRegistryReadyEvent.class) + .matches(server.getPluginManager()); + + Assertions.assertTrue(postConfig, "Event did not fire after the config phase"); + } + + @AfterEach + public void pluginTeardown() { + if (plugin != null) { + server.getPluginManager().disablePlugin(plugin); + plugin = null; + } + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/config/DefaultConfigTests.java b/src/test/java/xyz/alexcrea/cuanvil/config/DefaultConfigTests.java new file mode 100644 index 0000000..ac3f73e --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/config/DefaultConfigTests.java @@ -0,0 +1,89 @@ +package xyz.alexcrea.cuanvil.config; + +import io.delilaheve.util.ConfigOptions; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import org.bukkit.NamespacedKey; +import org.bukkit.configuration.file.FileConfiguration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry; +import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity; +import xyz.alexcrea.cuanvil.tests.SharedCustomAnvilTest; + +import java.util.stream.Stream; + +public class DefaultConfigTests extends SharedCustomAnvilTest { + + @ParameterizedTest + @MethodSource("provideStringsForIsConfiguredValueValid") + public void isConfiguredValueValid(String path, Object value){ + FileConfiguration config = ConfigHolder.DEFAULT_CONFIG.getConfig(); + + Assertions.assertEquals(value.toString(), config.getString(path), "Default value is not the same as in the config file"); + } + + @ParameterizedTest + @MethodSource("provideForEnchantmentsTests") + public void testDefaultEnchantLimit(NamespacedKey key){ + CAEnchantment enchantment = CAEnchantmentRegistry.getInstance().getByKey(key); + Assertions.assertNotNull(enchantment, "Enchantment was somehow not found"); + + int levelLimit = ConfigOptions.INSTANCE.enchantLimit(enchantment); + int mergeLimit = ConfigOptions.INSTANCE.maxBeforeMergeDisabled(enchantment); + + Assertions.assertEquals(enchantment.defaultMaxLevel(), levelLimit,"Default enchantment limit is not the same as expected limit "); + + if(mergeLimit >= 0) { // negative mean default + Assertions.assertEquals(enchantment.defaultMaxLevel(), mergeLimit,"Default enchantment merge limit is not the same as expected limit "); + } + + } + + @ParameterizedTest + @MethodSource("provideForEnchantmentsTests") + public void testDefaultEnchantValues(NamespacedKey key){ + CAEnchantment enchantment = CAEnchantmentRegistry.getInstance().getByKey(key); + Assertions.assertNotNull(enchantment, "Enchantment was somehow not found"); + + int itemValue = ConfigOptions.INSTANCE.enchantmentValue(enchantment, false); + int bookValue = ConfigOptions.INSTANCE.enchantmentValue(enchantment, true); + + EnchantmentRarity rarity = enchantment.defaultRarity(); + + Assertions.assertEquals(rarity.getItemValue(), itemValue,"Default enchantment item value is not the same as expected value"); + Assertions.assertEquals(rarity.getBookValue(), bookValue,"Default enchantment book value is not the same as expected value"); + } + + + private static Stream provideStringsForIsConfiguredValueValid() { + return Stream.of( + // Default options + Arguments.of(ConfigOptions.CAP_ANVIL_COST, ConfigOptions.DEFAULT_CAP_ANVIL_COST), + Arguments.of(ConfigOptions.CAP_ANVIL_COST, ConfigOptions.DEFAULT_CAP_ANVIL_COST), + Arguments.of(ConfigOptions.MAX_ANVIL_COST, ConfigOptions.DEFAULT_MAX_ANVIL_COST), + Arguments.of(ConfigOptions.REMOVE_ANVIL_COST_LIMIT, ConfigOptions.DEFAULT_REMOVE_ANVIL_COST_LIMIT), + Arguments.of(ConfigOptions.REPLACE_TOO_EXPENSIVE, ConfigOptions.DEFAULT_REPLACE_TOO_EXPENSIVE), + Arguments.of(ConfigOptions.ITEM_REPAIR_COST, ConfigOptions.DEFAULT_ITEM_REPAIR_COST), + Arguments.of(ConfigOptions.UNIT_REPAIR_COST, ConfigOptions.DEFAULT_UNIT_REPAIR_COST), + Arguments.of(ConfigOptions.ITEM_RENAME_COST, ConfigOptions.DEFAULT_ITEM_RENAME_COST), + Arguments.of(ConfigOptions.SACRIFICE_ILLEGAL_COST, ConfigOptions.DEFAULT_SACRIFICE_ILLEGAL_COST), + // Color options + Arguments.of(ConfigOptions.ALLOW_COLOR_CODE, ConfigOptions.DEFAULT_ALLOW_COLOR_CODE), + Arguments.of(ConfigOptions.ALLOW_HEXADECIMAL_COLOR, ConfigOptions.DEFAULT_ALLOW_HEXADECIMAL_COLOR), + Arguments.of(ConfigOptions.PERMISSION_NEEDED_FOR_COLOR, ConfigOptions.DEFAULT_PERMISSION_NEEDED_FOR_COLOR), + Arguments.of(ConfigOptions.USE_OF_COLOR_COST, ConfigOptions.DEFAULT_USE_OF_COLOR_COST) + + ); + } + + // Test every enchantment defaults + private static Stream provideForEnchantmentsTests() { + return RegistryAccess.registryAccess().getRegistry(RegistryKey.ENCHANTMENT) + .stream().map(enchantment -> Arguments.of(enchantment.getKey())); + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/data/AnvilClickTestData.java b/src/test/java/xyz/alexcrea/cuanvil/data/AnvilClickTestData.java new file mode 100644 index 0000000..002b194 --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/data/AnvilClickTestData.java @@ -0,0 +1,57 @@ +package xyz.alexcrea.cuanvil.data; + +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.inventory.AnvilInventory; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.util.AnvilFuseTestUtil; + +public record AnvilClickTestData( + @Nullable ItemStack leftItem, + @Nullable ItemStack rightItem, + @Nullable ItemStack resultSlotItem, + @Nullable ItemStack expectedCursor, + int levelCost, + @Nullable Event.Result expectedResult, + boolean testNoLevelNoChange, + @Nullable Event.Result npChangeResult +) { + + public AnvilClickTestData(@Nullable ItemStack leftItem, + @Nullable ItemStack rightItem, + @Nullable ItemStack resultSlotItem, + @Nullable ItemStack expectedCursor, + int levelCost, + @Nullable Event.Result expectedResult) { + this(leftItem, rightItem, resultSlotItem, + expectedCursor, levelCost, expectedResult, + false, null); + } + + public AnvilClickTestData(@Nullable ItemStack leftItem, + @Nullable ItemStack rightItem, + @Nullable ItemStack resultSlotItem, + @Nullable ItemStack expectedCursor, + int levelCost) { + this(leftItem, rightItem, resultSlotItem, + expectedCursor, levelCost, null); + } + + public AnvilClickTestData(@Nullable ItemStack expectedCursor, + int levelCost, + @Nullable Event.Result expectedResult) { + this(null, null, null, + expectedCursor, levelCost, expectedResult, + false, null); + } + + public AnvilClickTestData(@Nullable ItemStack expectedCursor, + int levelCost) { + this(expectedCursor, levelCost, null); + } + + public void executeTest(AnvilInventory anvil, Player player){ + AnvilFuseTestUtil.executeAnvilClickTest(anvil, player, this); + } +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/data/AnvilFuseTestData.java b/src/test/java/xyz/alexcrea/cuanvil/data/AnvilFuseTestData.java new file mode 100644 index 0000000..8542d41 --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/data/AnvilFuseTestData.java @@ -0,0 +1,61 @@ +package xyz.alexcrea.cuanvil.data; + +import org.bukkit.entity.HumanEntity; +import org.bukkit.inventory.AnvilInventory; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.util.AnvilFuseTestUtil; + +public record AnvilFuseTestData( + @Nullable ItemStack leftItem, + @Nullable ItemStack rightItem, + @Nullable ItemStack expectedResult, + + @Nullable ItemStack expectedAfterLeftPlaced, + @Nullable ItemStack expectedAfterRightPlaced, + + @Nullable Integer expectedPriceAfterLeftPlaced, + @Nullable Integer expectedPriceAfterRightPlaced, + @Nullable Integer expectedPriceAfterBothPlaced + ){ + + public AnvilFuseTestData( + @Nullable ItemStack leftItem, + @Nullable ItemStack rightItem, + @Nullable ItemStack expectedResult, + @Nullable Integer expectedPriceAfterBothPlaced + ){ + this(leftItem, rightItem, expectedResult, + null, null, null, null, + expectedPriceAfterBothPlaced + ); + } + + public AnvilFuseTestData( + @Nullable ItemStack leftItem, + @Nullable ItemStack rightItem, + @Nullable ItemStack expectedResult + ){ + this(leftItem, rightItem, expectedResult, null + ); + } + + public AnvilFuseTestData( + @Nullable ItemStack leftItem, + @Nullable ItemStack rightItem, + @Nullable ItemStack expectedResult, + + @Nullable ItemStack expectedAfterLeftPlaced, + @Nullable ItemStack expectedAfterRightPlaced + ){ + this(leftItem, rightItem, + expectedResult, expectedAfterLeftPlaced, expectedAfterRightPlaced, + null, null, null + ); + } + + public void executeTest(AnvilInventory anvil, HumanEntity player){ + AnvilFuseTestUtil.executeAnvilFuseTest(anvil, player, this); + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/data/TestDataContainer.java b/src/test/java/xyz/alexcrea/cuanvil/data/TestDataContainer.java new file mode 100644 index 0000000..b4f47a5 --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/data/TestDataContainer.java @@ -0,0 +1,250 @@ +package xyz.alexcrea.cuanvil.data; + +import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.AnvilInventory; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Assertions; +import xyz.alexcrea.cuanvil.util.AnvilFuseTestUtil; + +@SuppressWarnings("unused") +public record TestDataContainer( + @NotNull AnvilFuseTestData fuseData, + @Nullable AnvilClickTestData clickData +) { + + public void executeTest(AnvilInventory anvil, Player player) { + fuseData.executeTest(anvil, player); + if (clickData != null) clickData.executeTest(anvil, player); + } + + public void executeFuseTest(AnvilInventory anvil, HumanEntity player) { + fuseData.executeTest(anvil, player); + } + + public void executeClickTest(AnvilInventory anvil, Player player) { + Assertions.assertNotNull(clickData); + clickData.executeTest(anvil, player); + } + + public @NotNull TestDataContainer nullifyResult() { + return new TestDataContainer( + new AnvilFuseTestData( + fuseData.leftItem(), fuseData.rightItem(), + null + ), null); + } + + public @NotNull TestDataContainer setCost( + @Nullable Integer priceAfterLeft, + @Nullable Integer priceAfterRight, + int priceAfterBoth + ) { + AnvilFuseTestData data = new AnvilFuseTestData( + fuseData.leftItem(), fuseData.rightItem(), fuseData.expectedResult(), + fuseData.expectedAfterLeftPlaced(), + fuseData.expectedAfterRightPlaced(), + priceAfterLeft, + priceAfterRight, + priceAfterBoth + ); + + AnvilClickTestData CData; + if (clickData == null) { + CData = null; + } else { + CData = new AnvilClickTestData( + clickData.leftItem(), clickData.rightItem(), clickData.resultSlotItem(), + clickData.expectedCursor(), priceAfterBoth, + clickData.expectedResult(), + clickData.testNoLevelNoChange(), clickData.npChangeResult() + ); + } + return new TestDataContainer(data, CData); + } + + public @NotNull TestDataContainer setCost( + int priceAfterBoth + ) { + return setCost(null, null, priceAfterBoth); + } + + // Set fuse items + public @NotNull TestDataContainer setFuseItems(@Nullable ItemStack left, @Nullable ItemStack right, @Nullable ItemStack expected) { + AnvilFuseTestData data = new AnvilFuseTestData( + left, right, expected, + fuseData.expectedAfterLeftPlaced(), + fuseData.expectedAfterRightPlaced(), + fuseData.expectedPriceAfterLeftPlaced(), + fuseData.expectedPriceAfterRightPlaced(), + fuseData.expectedPriceAfterBothPlaced() + ); + return new TestDataContainer(data, clickData); + } + + public @NotNull TestDataContainer setFuseItems( + @Nullable ItemStack left, @Nullable ItemStack right, @Nullable ItemStack expected, + @Nullable ItemStack leftExpected, @Nullable ItemStack rightExpected) { + AnvilFuseTestData data = new AnvilFuseTestData( + left, right, expected, + leftExpected, + rightExpected, + fuseData.expectedPriceAfterLeftPlaced(), + fuseData.expectedPriceAfterRightPlaced(), + fuseData.expectedPriceAfterBothPlaced() + ); + return new TestDataContainer(data, clickData); + } + + public @NotNull TestDataContainer setFuseLeft(@Nullable ItemStack left) { + AnvilFuseTestData data = new AnvilFuseTestData( + left, fuseData.rightItem(), fuseData.expectedResult(), + fuseData.expectedAfterLeftPlaced(), + fuseData.expectedAfterRightPlaced(), + fuseData.expectedPriceAfterLeftPlaced(), + fuseData.expectedPriceAfterRightPlaced(), + fuseData.expectedPriceAfterBothPlaced() + ); + return new TestDataContainer(data, clickData); + } + + public @NotNull TestDataContainer setFuseRight(@Nullable ItemStack right) { + AnvilFuseTestData data = new AnvilFuseTestData( + fuseData.leftItem(), right, fuseData.expectedResult(), + fuseData.expectedAfterLeftPlaced(), + fuseData.expectedAfterRightPlaced(), + fuseData.expectedPriceAfterLeftPlaced(), + fuseData.expectedPriceAfterRightPlaced(), + fuseData.expectedPriceAfterBothPlaced() + ); + return new TestDataContainer(data, clickData); + } + + public @NotNull TestDataContainer setFuseExpected(@Nullable ItemStack expected) { + AnvilFuseTestData data = new AnvilFuseTestData( + fuseData.leftItem(), fuseData.rightItem(), expected, + fuseData.expectedAfterLeftPlaced(), + fuseData.expectedAfterRightPlaced(), + fuseData.expectedPriceAfterLeftPlaced(), + fuseData.expectedPriceAfterRightPlaced(), + fuseData.expectedPriceAfterBothPlaced() + ); + return new TestDataContainer(data, clickData); + } + + public @NotNull TestDataContainer setFuseExpectedLeft(@Nullable ItemStack expected) { + AnvilFuseTestData data = new AnvilFuseTestData( + fuseData.leftItem(), fuseData.rightItem(), fuseData.expectedResult(), + expected, + fuseData.expectedAfterRightPlaced(), + fuseData.expectedPriceAfterLeftPlaced(), + fuseData.expectedPriceAfterRightPlaced(), + fuseData.expectedPriceAfterBothPlaced() + ); + return new TestDataContainer(data, clickData); + } + + public @NotNull TestDataContainer setFuseExpectedRight(@Nullable ItemStack expected) { + AnvilFuseTestData data = new AnvilFuseTestData( + fuseData.leftItem(), fuseData.rightItem(), fuseData.expectedResult(), + fuseData.expectedAfterLeftPlaced(), + expected, + fuseData.expectedPriceAfterLeftPlaced(), + fuseData.expectedPriceAfterRightPlaced(), + fuseData.expectedPriceAfterBothPlaced() + ); + return new TestDataContainer(data, clickData); + } + + // Set click items + public @NotNull TestDataContainer setClickLeft(@Nullable ItemStack left) { + if (clickData == null) return this; + AnvilClickTestData data = new AnvilClickTestData( + left, clickData.rightItem(), clickData.resultSlotItem(), clickData.expectedCursor(), + clickData.levelCost(), clickData.expectedResult(), + clickData.testNoLevelNoChange(), clickData.npChangeResult() + ); + + return new TestDataContainer(fuseData, data); + } + + public @NotNull TestDataContainer setClickRight(@Nullable ItemStack right) { + if (clickData == null) return this; + AnvilClickTestData data = new AnvilClickTestData( + clickData.leftItem(), right, clickData.resultSlotItem(), clickData.expectedCursor(), + clickData.levelCost(), clickData.expectedResult(), + clickData.testNoLevelNoChange(), clickData.npChangeResult() + ); + + return new TestDataContainer(fuseData, data); + } + + public @NotNull TestDataContainer setClickOutput(@Nullable ItemStack output) { + if (clickData == null) return this; + AnvilClickTestData data = new AnvilClickTestData( + clickData.leftItem(), clickData.rightItem(), output, clickData.expectedCursor(), + clickData.levelCost(), clickData.expectedResult(), + clickData.testNoLevelNoChange(), clickData.npChangeResult() + ); + + return new TestDataContainer(fuseData, data); + } + + public @NotNull TestDataContainer setClickCursor(@Nullable ItemStack cursor) { + if (clickData == null) return this; + AnvilClickTestData data = new AnvilClickTestData( + clickData.leftItem(), clickData.rightItem(), clickData.resultSlotItem(), cursor, + clickData.levelCost(), clickData.expectedResult(), + clickData.testNoLevelNoChange(), clickData.npChangeResult() + ); + + return new TestDataContainer(fuseData, data); + } + + // Both item + public @NotNull TestDataContainer setExpectedResult(@Nullable ItemStack result) { + return setFuseExpected(result).setClickCursor(result); + } + + // Get fuse item + public @Nullable ItemStack getLeftFuse() { + return fuseData.leftItem(); + } + + public @Nullable ItemStack getRightFuse() { + return fuseData.rightItem(); + } + + public @Nullable ItemStack getExpectedFuse() { + return fuseData.expectedResult(); + } + + public @Nullable ItemStack getLeftExpectedFuse() { + return fuseData.expectedAfterLeftPlaced(); + } + + public @Nullable ItemStack getRightExpectedFuse() { + return fuseData.expectedAfterRightPlaced(); + } + + // Get click item + public @Nullable ItemStack getLeftClick() { + return clickData == null ? null : clickData.leftItem(); + } + + public @Nullable ItemStack getRightClick() { + return clickData == null ? null : clickData.rightItem(); + } + + public @Nullable ItemStack getOutputClick() { + return clickData == null ? null : clickData.resultSlotItem(); + } + + public @Nullable ItemStack getCursorClick() { + return clickData == null ? null : clickData.expectedCursor(); + } + + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/mock/AnvilViewMock.java b/src/test/java/xyz/alexcrea/cuanvil/mock/AnvilViewMock.java new file mode 100644 index 0000000..afcbb26 --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/mock/AnvilViewMock.java @@ -0,0 +1,81 @@ +package xyz.alexcrea.cuanvil.mock; + +import org.bukkit.entity.HumanEntity; +import org.bukkit.inventory.AnvilInventory; +import org.bukkit.inventory.view.AnvilView; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.mockbukkit.mockbukkit.inventory.PlayerInventoryViewMock; + +@SuppressWarnings({"removal"}) +public class AnvilViewMock extends PlayerInventoryViewMock implements AnvilView { + + private @NotNull AnvilInventory top; + + /** + * Constructs a new {@link PlayerInventoryViewMock} for the provided player, with the specified top inventory. + * + * @param player The player to create the view for. + * @param top The top inventory. + */ + public AnvilViewMock(@NotNull HumanEntity player, @NotNull AnvilInventory top) { + super(player, top); + this.top = top; + } + + @Override + public @Nullable String getRenameText() { + return top.getRenameText(); + } + + @Override + public int getRepairItemCountCost() { + return top.getRepairCostAmount(); + } + + @Override + public int getRepairCost() { + return top.getRepairCost(); + } + + @Override + public int getMaximumRepairCost() { + return top.getMaximumRepairCost(); + } + + @Override + public void setRepairItemCountCost(int amount) { + top.setRepairCostAmount(amount); + } + + @Override + public void setRepairCost(int cost) { + top.setRepairCost(cost); + } + + @Override + public void setMaximumRepairCost(int levels) { + top.setMaximumRepairCost(levels); + } + + @Override + public boolean bypassesEnchantmentLevelRestriction() { + throw new UnsupportedOperationException("This is currently is not used in CustomAnvil"); + } + + @Override + public void bypassEnchantmentLevelRestriction(boolean bypassEnchantmentLevelRestriction) { + throw new UnsupportedOperationException("This is currently is not used in CustomAnvil"); + } + + @Override + public @NotNull AnvilInventory getTopInventory() { + return top; + } + + @Override + public void open() { + throw new UnsupportedOperationException("This is currently is not used in CustomAnvil"); + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/tests/ConfigResetCustomAnvilTest.java b/src/test/java/xyz/alexcrea/cuanvil/tests/ConfigResetCustomAnvilTest.java new file mode 100644 index 0000000..3c03bb4 --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/tests/ConfigResetCustomAnvilTest.java @@ -0,0 +1,38 @@ +package xyz.alexcrea.cuanvil.tests; + +import org.junit.jupiter.api.AfterEach; +import xyz.alexcrea.cuanvil.config.ConfigHolder; + +import java.io.File; + +public abstract class ConfigResetCustomAnvilTest extends DefaultCustomAnvilTest { + + @Override + @AfterEach + public void tearDown() { + // Destroy saved config file + String[] configs = new String[]{ + "config.yml", + "item_groups.yml", + "enchant_conflict.yml", + "unit_repair_item.yml", + "custom_recipes.yml" + }; + for (String config : configs) { + File configFile = new File(plugin.getDataFolder(), config); + configFile.delete(); + } + + // Set config to null + ConfigHolder.DEFAULT_CONFIG = null; + ConfigHolder.ITEM_GROUP_HOLDER = null; + ConfigHolder.CONFLICT_HOLDER = null; + ConfigHolder.UNIT_REPAIR_HOLDER = null; + ConfigHolder.CUSTOM_RECIPE_HOLDER = null; + + // Do parent works + super.tearDown(); + } + + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/tests/DefaultCustomAnvilTest.java b/src/test/java/xyz/alexcrea/cuanvil/tests/DefaultCustomAnvilTest.java new file mode 100644 index 0000000..bdb280c --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/tests/DefaultCustomAnvilTest.java @@ -0,0 +1,54 @@ +package xyz.alexcrea.cuanvil.tests; + +import io.delilaheve.CustomAnvil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry; + +import java.util.ArrayList; +import java.util.List; + +public abstract class DefaultCustomAnvilTest { + + protected static ServerMock server; + protected CustomAnvil plugin; + + @BeforeAll + public static void setupMock() { + server = MockBukkit.mock(); + } + + @BeforeEach + public void setUp() { + // Load your plugin + plugin = MockBukkit.load(CustomAnvil.class); + // Continue initialization of the plugin + server.getScheduler().performOneTick(); + } + + @AfterEach + public void tearDown() { + // Unregister enchantments + List toUnregister = new ArrayList<>( + CAEnchantmentRegistry.getInstance().values() + ); + + for (CAEnchantment caEnchantment : toUnregister) { + CAEnchantmentRegistry.getInstance().unregister(caEnchantment); + } + + server.getPluginManager().disablePlugin(plugin); + } + + @AfterAll + public static void tearDownMock() { + // Stop the mock server + MockBukkit.unmock(); + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/tests/SharedCustomAnvilTest.java b/src/test/java/xyz/alexcrea/cuanvil/tests/SharedCustomAnvilTest.java new file mode 100644 index 0000000..02844f4 --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/tests/SharedCustomAnvilTest.java @@ -0,0 +1,45 @@ +package xyz.alexcrea.cuanvil.tests; + +import io.delilaheve.CustomAnvil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry; + +import java.util.ArrayList; +import java.util.List; + +public abstract class SharedCustomAnvilTest { + + protected static ServerMock server; + protected static CustomAnvil plugin; + + @BeforeAll + public static void setUp() { + // Start the mock server + server = MockBukkit.mock(); + // Load your plugin + plugin = MockBukkit.load(CustomAnvil.class); + // Continue initialization of the plugin + server.getScheduler().performOneTick(); + } + + @AfterAll + public static void tearDown() { + // Stop the mock server + MockBukkit.unmock(); + + // Unregister enchantments + List toUnregister = new ArrayList<>( + CAEnchantmentRegistry.getInstance().values() + ); + + for (CAEnchantment caEnchantment : toUnregister) { + CAEnchantmentRegistry.getInstance().unregister(caEnchantment); + } + + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/tests/SharedOnlyMockBukkit.java b/src/test/java/xyz/alexcrea/cuanvil/tests/SharedOnlyMockBukkit.java new file mode 100644 index 0000000..0f38239 --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/tests/SharedOnlyMockBukkit.java @@ -0,0 +1,24 @@ +package xyz.alexcrea.cuanvil.tests; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; + +public class SharedOnlyMockBukkit { + + protected static ServerMock server; + + @BeforeAll + public static void setUp() { + // Start the mock server + server = MockBukkit.mock(); + } + + @AfterAll + public static void tearDown() { + // Stop the mock server + MockBukkit.unmock(); + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/util/AnvilFuseTestUtil.java b/src/test/java/xyz/alexcrea/cuanvil/util/AnvilFuseTestUtil.java new file mode 100644 index 0000000..6f5c7bb --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/util/AnvilFuseTestUtil.java @@ -0,0 +1,271 @@ +package xyz.alexcrea.cuanvil.util; + +import io.delilaheve.util.ItemUtil; +import org.bukkit.Material; +import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.inventory.*; +import org.bukkit.inventory.AnvilInventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.Repairable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Assertions; +import xyz.alexcrea.cuanvil.data.AnvilClickTestData; +import xyz.alexcrea.cuanvil.data.AnvilFuseTestData; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.listener.AnvilResultListener; +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener; +import xyz.alexcrea.cuanvil.mock.AnvilViewMock; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class AnvilFuseTestUtil { + + private static PrepareAnvilListener PREPARE_LISTENER = new PrepareAnvilListener(); + private static AnvilResultListener RESULT_LISTENER = new AnvilResultListener(); + + public static ItemStack prepareItem(@NotNull Material material, + @NotNull List enchantments, + @NotNull List level) { + return prepareItem(material, 0, enchantments, level); + } + + public static ItemStack prepareItem(@NotNull Material material, + int repairCost, + @NotNull List enchantments, + @NotNull List level) { + Assertions.assertEquals(enchantments.size(), level.size()); + + HashMap enchantmentMap = new HashMap<>(); + for (int i = 0; i < enchantments.size(); i++) { + enchantmentMap.put(enchantments.get(i), level.get(i)); + } + + ItemStack item = new ItemStack(material); + ItemUtil.INSTANCE.setEnchantmentsUnsafe(item, enchantmentMap); + + ItemMeta meta = item.getItemMeta(); + ((Repairable) meta).setRepairCost(repairCost); + item.setItemMeta(meta); + + return item; + } + + + public static ItemStack prepareItem(@NotNull Material material, + @NotNull List enchantmentNames, + Integer... levels) { + return prepareItem(material, 0, enchantmentNames, levels); + } + + public static ItemStack prepareItem(@NotNull Material material, + int repairCost, + @NotNull List enchantmentNames, + Integer... levels) { + List enchantments = new ArrayList<>(); + + for (String enchantmentName : enchantmentNames) { + List enchantmentList = CAEnchantment.getListByName(enchantmentName); + Assertions.assertNotEquals(0, enchantmentList.size(), + "Could not find enchantment \"" + enchantmentName + "\""); + + enchantments.addAll(enchantmentList); + } + + return prepareItem(material, repairCost, enchantments, List.of(levels)); + } + + + /* + * Need to use that as it seems setting item in the inventory will not trigger the anvil click even + * + * Not the best for non-custom anvil plugins but work in the context of CA + */ + public static void imitateAnvilUpdate( + @NotNull HumanEntity player, + @NotNull AnvilInventory anvil) { + + AnvilViewMock view = new AnvilViewMock(player, anvil); + try { + PrepareAnvilEvent event = new PrepareAnvilEvent(view, anvil.getItem(2)); + + // Not ideal but possible and the easiest so why not + PREPARE_LISTENER.anvilCombineCheck(event); + anvil.setResult(event.getResult()); + } catch (Exception e) { + Assertions.fail(e); + } + } + + public static void executeAnvilFuseTest( + @NotNull AnvilInventory anvil, + @NotNull HumanEntity player, + @NotNull AnvilFuseTestData data + ) { + Assertions.assertEquals(player.getOpenInventory().getTopInventory(), anvil, + "Openned inventory is not anvil"); + + ItemStack afterLeft = data.expectedAfterLeftPlaced(); + ItemStack afterRight = data.expectedAfterRightPlaced(); + ItemStack afterBoth = data.expectedResult(); + // Fist, test null result(s) + + // Test with only the left item + if(afterLeft == null){ + anvil.setItem(1, null); // We clear the right slot in case something was there + testPlacingItem(anvil, player, + 0, data.expectedPriceAfterLeftPlaced(), + data.leftItem(), null); + } + + // Test with only the right item + if(afterRight == null){ + anvil.setItem(0, null); // We only want the right item. so we remove the left one + testPlacingItem(anvil, player, + 1, data.expectedPriceAfterRightPlaced(), + data.rightItem(), null); + } + + // Test with both placed + if(afterBoth == null){ + anvil.setItem(0, data.leftItem()); + testPlacingItem(anvil, player, + 1, data.expectedPriceAfterBothPlaced(), + data.rightItem(), data.expectedResult()); + } + + // Then, test non null result(s) + + // Test with only the left item + if(afterLeft != null){ + anvil.setItem(1, null); // We clear the right slot in case something was there + testPlacingItem(anvil, player, + 0, data.expectedPriceAfterLeftPlaced(), + data.leftItem(), afterLeft); + } + + // Test with only the right item + if(afterRight != null){ + anvil.setItem(0, null); // We only want the right item. so we remove the left one + testPlacingItem(anvil, player, + 1, data.expectedPriceAfterRightPlaced(), + data.rightItem(), afterRight); + } + + // Test with both placed + if(afterBoth != null){ + anvil.setItem(0, data.leftItem()); + testPlacingItem(anvil, player, + 1, data.expectedPriceAfterBothPlaced(), + data.rightItem(), afterBoth); + } + } + + public static void executeAnvilClickTest( + @NotNull AnvilInventory anvil, + @NotNull Player player, + @NotNull AnvilClickTestData data + ) { + if (data.testNoLevelNoChange()) { + ItemStack left = anvil.getFirstItem(); + ItemStack right = anvil.getSecondItem(); + ItemStack result = anvil.getResult(); + + player.setLevel(0); + player.setExp(0); + player.setItemOnCursor(null); + + // Do a test with not enough level + simulateClick(anvil, player, data.npChangeResult()); + + // Nothing should have changed + assertEqual(left, anvil.getFirstItem()); + assertEqual(right, anvil.getSecondItem()); + assertEqual(result, anvil.getResult()); + assertEqual(null, player.getItemOnCursor()); + } + player.setLevel(data.levelCost()); + player.setExp(0); + player.setItemOnCursor(null); + + simulateClick(anvil, player, data.expectedResult()); + + // Should have simulated the click + assertEqual(data.leftItem(), anvil.getFirstItem()); + assertEqual(data.rightItem(), anvil.getSecondItem()); + assertEqual(data.resultSlotItem(), anvil.getResult()); + assertEqual(data.expectedCursor(), data.expectedCursor()); + + // Test if the player has no more xp + Assertions.assertEquals(0, player.getLevel(), "Player has more level than expected"); + } + + private static void simulateClick( + @NotNull AnvilInventory anvil, + @NotNull Player player, + @Nullable Event.Result expectedResult + ) { + AnvilViewMock view = new AnvilViewMock(player, anvil); + try { + InventoryClickEvent event = new InventoryClickEvent(view, + InventoryType.SlotType.RESULT, + PrepareAnvilListener.ANVIL_OUTPUT_SLOT, + ClickType.LEFT, + InventoryAction.PICKUP_ALL); + + RESULT_LISTENER.anvilExtractionCheck(event); + if (expectedResult != null) { + Assertions.assertEquals(expectedResult, event.getResult()); + } + } catch (Exception e) { + Assertions.fail(e); + } + } + + @SuppressWarnings({"removal"}) + private static void testPlacingItem( + @NotNull AnvilInventory anvil, + @NotNull HumanEntity player, + int slot, + Integer expectedPrice, + @Nullable ItemStack toPlace, + @Nullable ItemStack expectedResult) { + anvil.setItem(slot, toPlace); + anvil.setItem(2, null); + AnvilFuseTestUtil.imitateAnvilUpdate(player, anvil); + + ItemStack result = anvil.getItem(2); + assertEqual(expectedResult, result); + + assertPriceEqual(expectedPrice, anvil.getRepairCost()); + } + + public static void assertEqual(@Nullable ItemStack expected, @Nullable ItemStack other) { + boolean secondIsAir = isAir(other); + if (isAir(expected)) + Assertions.assertTrue(secondIsAir, "Item " + other + " was not air but was expected to be."); + else { + Assertions.assertFalse(secondIsAir, "Item " + other + " is air but was expected to be " + expected); + + expected.setDurability(expected.getDurability()); + other.setDurability(other.getDurability()); + Assertions.assertEquals(expected, other); + } + + } + + public static boolean isAir(@Nullable ItemStack item) { + return item == null || item.isEmpty() || item.getAmount() == 0; + } + + public static void assertPriceEqual(Integer expectedPrice, int price) { + if (expectedPrice == null) return; + Assertions.assertEquals(expectedPrice, price, "Price of anvil fuse was wrong"); + } + +} diff --git a/src/test/java/xyz/alexcrea/cuanvil/util/CommonItemUtil.java b/src/test/java/xyz/alexcrea/cuanvil/util/CommonItemUtil.java new file mode 100644 index 0000000..1228cda --- /dev/null +++ b/src/test/java/xyz/alexcrea/cuanvil/util/CommonItemUtil.java @@ -0,0 +1,27 @@ +package xyz.alexcrea.cuanvil.util; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.List; + +public class CommonItemUtil { + + public static ItemStack sharpness(int level){ + return AnvilFuseTestUtil.prepareItem( + Material.DIAMOND_SWORD, + List.of("sharpness"), + level + ); + } + + public static ItemStack bane_of_arthropods(int level){ + return AnvilFuseTestUtil.prepareItem( + Material.DIAMOND_SWORD, + List.of("bane_of_arthropods"), + level + ); + } + + +} diff --git a/src/test/resources/plugin.yml b/src/test/resources/plugin.yml new file mode 100644 index 0000000..37fb95a --- /dev/null +++ b/src/test/resources/plugin.yml @@ -0,0 +1,73 @@ +main: io.delilaheve.CustomAnvil +name: CustomAnvil +prefix: "Custom Anvil" +version: test +folia-supported: true +description: Allow to customise anvil mechanics +api-version: 1.16 +load: POSTWORLD +authors: [ DelilahEve, alexcrea ] +libraries: + - org.jetbrains.kotlin:kotlin-stdlib:2.0.21 + +commands: + customanvil: + description: Generic command for custom anvil + anvilconfigreload: + description: Reload every config of this plugin + permission: ca.command.reload + aliases: + #- acreload # anvil config reload + #- careload # custom anvil reload + - carl # custom anvil reload + customanvilconfig: + description: open a menu for administrator to edit plugin's config in game + permission: ca.config.edit + aliases: + - configanvil + +permissions: + ca.affected: + default: true + description: Player with this permission will be affected by the plugin + ca.bypass.fuse: + default: false + description: Allow player to combine every enchantments to every item (no custom limit) + ca.bypass.level: + default: false + description: Allow player to bypass every level limit (no custom limit) + ca.command.reload: + default: op + description: Allow administrator to reload the plugin's configs + ca.command.diagnostic: + default: op + description: Get debug information about the plugin and server + ca.config.edit: + default: op + description: Allow administrator to edit the plugin's config in game + # color permissions + ca.color.code: + default: op + description: Allow player to use color code if enabled (toggleable) + ca.color.hex: + default: op + description: Allow player to use hexadecimal color if enabled (toggleable) + # lore edit permissions + ca.lore_edit.book: + default: op + description: Allow player to edit lore via book and quil if enabled (toggleable) + ca.lore_edit.paper: + default: op + description: Allow player to edit lore via paper if enabled (toggleable) + +# soft depend on old name of this plugin (UnsafeEnchantsPlus), so I can disable it if it is on the same server +# Also depend to other plugin for compatibility +softdepend: + - UnsafeEnchantsPlus + - ProtocolLib + - Disenchantment + - EnchantsSquared + - EcoEnchants + - eco + - ExcellentEnchants + - HavenBags