From a6cee2d59121eb1fa7e7158390be2a2a43086eb2 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 30 Dec 2025 02:03:58 +0100 Subject: [PATCH 001/166] check air --- .../cuanvil/listener/PrepareAnvilListener.kt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index c47d828..04323e8 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -40,6 +40,10 @@ class PrepareAnvilListener : Listener { const val ANVIL_OUTPUT_SLOT = 2 } + private fun ItemStack?.isAir(): Boolean { + return this == null || this.isEmpty + } + /** * Event handler logic for when an anvil contains items to be combined */ @@ -79,10 +83,11 @@ class PrepareAnvilListener : Listener { if (testCustomRecipe(event, inventory, player, first, second)) return // Test rename lonely item - if (second == null) { + if (second.isAir()) { doRenaming(event, inventory, player, first) return } + second as ItemStack // not air we know it's not null // Test for merge if (first.canMergeWith(second)) { @@ -102,7 +107,7 @@ class PrepareAnvilListener : Listener { } private fun isImmutable(item: ItemStack?): Boolean { - if (item == null) return false + if (item.isAir()) return false val meta = item.itemMeta return meta != null && @@ -153,7 +158,7 @@ class PrepareAnvilListener : Listener { if (finalResult == null) return false event.result = finalResult.result - if (finalResult.result == null) return false + if (finalResult.result.isAir()) return false AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost, true) return true @@ -179,7 +184,7 @@ class PrepareAnvilListener : Listener { if (finalResult == null) return event.result = finalResult.result - if (finalResult.result == null) return + if (finalResult.result.isAir()) return AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost) } @@ -267,7 +272,7 @@ class PrepareAnvilListener : Listener { if (finalResult == null) return event.result = finalResult.result - if (finalResult.result == null) return + if (finalResult.result.isAir()) return AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost) } @@ -312,7 +317,7 @@ class PrepareAnvilListener : Listener { if (finalResult == null) return false event.result = finalResult.result - if (finalResult.result == null) return false + if (finalResult.result.isAir()) return false AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost) return true @@ -332,7 +337,7 @@ class PrepareAnvilListener : Listener { result = AnvilLoreEditUtil.tryLoreEditByPaper(player, first, second, xpCost) } - if (result == null || first == result) { + if (result.isAir() || first == result) { CustomAnvil.log("lore edit, But input is same as output") event.result = null return false From 161ef6ba912262805aecce1a45cee318be8400e3 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 30 Dec 2025 02:18:11 +0100 Subject: [PATCH 002/166] fix forgot --- .../xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index 04323e8..ebf16ed 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -41,7 +41,7 @@ class PrepareAnvilListener : Listener { } private fun ItemStack?.isAir(): Boolean { - return this == null || this.isEmpty + return this == null || this.type.isAir || this.amount == 0 } /** @@ -87,10 +87,9 @@ class PrepareAnvilListener : Listener { doRenaming(event, inventory, player, first) return } - second as ItemStack // not air we know it's not null // Test for merge - if (first.canMergeWith(second)) { + if (first.canMergeWith(second!!)) { doMerge(event, inventory, player, first, second) return } @@ -109,7 +108,7 @@ class PrepareAnvilListener : Listener { private fun isImmutable(item: ItemStack?): Boolean { if (item.isAir()) return false - val meta = item.itemMeta + val meta = item!!.itemMeta return meta != null && (hasImmutableEnchants(meta) || hasImmutableStoredEnchants(meta)) } From fe2196626ad55eddddc000123149a9800fd8fe7d Mon Sep 17 00:00:00 2001 From: alexcrea Date: Thu, 1 Jan 2026 17:15:49 +0100 Subject: [PATCH 003/166] bring back old gui tester --- .../cuanvil/dependency/gui/ExternGuiTester.kt | 16 ----- .../cuanvil/dependency/DependencyManager.kt | 4 +- .../dependency/gui/GenericExternGuiTester.kt | 58 +++++++++++++++++-- .../dependency/gui/GuiTesterSelector.kt | 2 +- 4 files changed, 55 insertions(+), 25 deletions(-) delete mode 100644 nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/ExternGuiTester.kt diff --git a/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/ExternGuiTester.kt b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/ExternGuiTester.kt deleted file mode 100644 index 8b06b10..0000000 --- a/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/ExternGuiTester.kt +++ /dev/null @@ -1,16 +0,0 @@ -package xyz.alexcrea.cuanvil.dependency.gui - -import org.bukkit.inventory.InventoryView - -interface ExternGuiTester { - - fun getContainerClass(view: InventoryView): Class? - - fun testIfGui(inventory: InventoryView): Boolean { - // container class only allow default bukkit craft view or test class - - val clazz = getContainerClass(inventory) - return clazz == null - } - -} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt index 66a9aa5..9c8c159 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt @@ -16,7 +16,7 @@ import xyz.alexcrea.cuanvil.api.event.listener.CAPreAnvilBypassEvent import xyz.alexcrea.cuanvil.api.event.listener.CATreatAnvilResultEvent import xyz.alexcrea.cuanvil.config.ConfigHolder import xyz.alexcrea.cuanvil.dependency.datapack.DataPackDependency -import xyz.alexcrea.cuanvil.dependency.gui.ExternGuiTester +import xyz.alexcrea.cuanvil.dependency.gui.GenericExternGuiTester import xyz.alexcrea.cuanvil.dependency.gui.GuiTesterSelector import xyz.alexcrea.cuanvil.dependency.packet.PacketManager import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerSelector @@ -34,7 +34,7 @@ object DependencyManager { lateinit var scheduler: TaskScheduler lateinit var packetManager: PacketManager - var externGuiTester: ExternGuiTester? = null + var externGuiTester: GenericExternGuiTester? = null var enchantmentSquaredCompatibility: EnchantmentSquaredDependency? = null var ecoEnchantCompatibility: EcoEnchantDependency? = null diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GenericExternGuiTester.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GenericExternGuiTester.kt index 4ff3354..0e430ef 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GenericExternGuiTester.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GenericExternGuiTester.kt @@ -1,14 +1,18 @@ 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: ExternGuiTester { +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 @@ -17,11 +21,7 @@ class GenericExternGuiTester: ExternGuiTester { var testedClass: String? = null lateinit var getHandleMethod: Method - override fun getContainerClass(view: InventoryView): Class? { - // In case we are in a test environment - if(!testExist) testClassExist() - if(inTesting) return view.javaClass //TEMPORARY - + private fun getContainerClass(view: InventoryView): Class? { if(!testedClass.contentEquals(view.javaClass.name)) return null @@ -58,4 +58,50 @@ class GenericExternGuiTester: ExternGuiTester { inTesting = true } + // Try if were in another plugin anvil inventory + fun testIfGui(inventory: InventoryView): Boolean { + // In case we are in a test environment + if(!testExist) testClassExist() + if(inTesting) 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/gui/GuiTesterSelector.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GuiTesterSelector.kt index f64a7f1..e445d8d 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GuiTesterSelector.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GuiTesterSelector.kt @@ -4,7 +4,7 @@ import xyz.alexcrea.cuanvil.update.UpdateUtils object GuiTesterSelector { - val selectGuiTester: ExternGuiTester? + val selectGuiTester: GenericExternGuiTester? get() { val versionParts = UpdateUtils.currentMinecraftVersionArray() if (versionParts[0] != 1) return null From a350b7fa698f54a667535f724463c547600f788e Mon Sep 17 00:00:00 2001 From: alexcrea Date: Thu, 1 Jan 2026 18:59:48 +0100 Subject: [PATCH 004/166] finally ! smaller jar is smaller --- build.gradle.kts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3affa9e..843753f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -148,9 +148,10 @@ tasks { val name = "${rootProject.name}-${effectiveVersion}.jar" archiveFileName.set(name) - // Exclude kotlin std and its annotation - exclude("**/kotlin-stdlib*.jar") - exclude("**/annotations*.jar") + // Exclude kotlin std, annotations and adventure api + exclude("*kotlin/**") + exclude("**/annotations/**") + exclude("net/kyori/**") // Shadow necessary dependency relocate("com.github.stefvanschie.inventoryframework", "xyz.alexcrea.inventoryframework") From a373cd76f760803c3208c7c19d5796ace86c8bbf Mon Sep 17 00:00:00 2001 From: alexcrea Date: Thu, 1 Jan 2026 19:01:13 +0100 Subject: [PATCH 005/166] has to add adventure as libary for spigot sadly --- build.gradle.kts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 843753f..25aec32 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -160,8 +160,10 @@ tasks { filesMatching("plugin.yml") { expand( "version" to effectiveVersion, - "libraries" to " \"org.jetbrains.kotlin:kotlin-stdlib:2.1.0\" " + - ", \"net.kyori:adventure-platform-bukkit:4.4.1\"" + "libraries" to " \"org.jetbrains.kotlin:kotlin-stdlib:2.1.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\"" ) } From 474ad0f1b2245d7b575160a454c0dcd95ca8ab89 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Thu, 1 Jan 2026 19:01:56 +0100 Subject: [PATCH 006/166] version up --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 25aec32..0412138 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,7 +18,7 @@ plugins { } group = "xyz.alexcrea" -version = "1.15.8" +version = "1.15.9" val effectiveVersion = "$version" + (if (System.getenv("SMALL_COMMIT_HASH") != null) "-dev-${System.getenv("SMALL_COMMIT_HASH")!!}" else "") From 4ed9de3d3c22060a9e41b47d813d01e9755c971a Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sat, 10 Jan 2026 19:09:03 +0100 Subject: [PATCH 007/166] FINALLY offline build SHOULD work --- build.gradle.kts | 67 ++++++++++++++++++------------------------------ 1 file changed, 25 insertions(+), 42 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 0412138..f005e45 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -140,30 +140,21 @@ allprojects { } + tasks { - // Online jar (use of libraries) - shadowJar { - // No suffix for this jar - val name = "${rootProject.name}-${effectiveVersion}.jar" + fun ShadowJar.configureBaseShadow(suffix: String, libraries: Array) { + val processedSuffix = if(suffix.isEmpty()) "" else "-$suffix" + val name = "${rootProject.name}-${effectiveVersion}${processedSuffix}.jar" archiveFileName.set(name) - // Exclude kotlin std, annotations and adventure api - exclude("*kotlin/**") - exclude("**/annotations/**") - exclude("net/kyori/**") - // Shadow necessary dependency relocate("com.github.stefvanschie.inventoryframework", "xyz.alexcrea.inventoryframework") - // Replace version and example fields in plugin.yml filesMatching("plugin.yml") { expand( - "version" to effectiveVersion, - "libraries" to " \"org.jetbrains.kotlin:kotlin-stdlib:2.1.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\"" + "version" to effectiveVersion + processedSuffix, + "libraries" to libraries.joinToString(transform = { "\"$it\"" }), ) } @@ -171,36 +162,28 @@ tasks { dependsOn(processResources) } - // Offline jar (include kotlin std in the final jar fine) - val offlineJar by // Shadow necessary dependency - registering( + // Online jar (use of libraries) + shadowJar { + configureBaseShadow("", + arrayOf( + "org.jetbrains.kotlin:kotlin-stdlib:2.1.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", + )) - // Include all project other dependencies - ShadowJar + // Exclude kotlin std, annotations and adventure api + exclude("*kotlin/**") + exclude("**/annotations/**") + exclude("net/kyori/**") + } - // Add custom anvil compiled - ::class, fun ShadowJar.() { - val name = "${rootProject.name}-${effectiveVersion}-offline.jar" - archiveFileName.set(name) + val offlineJar by registering(ShadowJar::class) { + configureBaseShadow("offline", emptyArray()) - // Shadow necessary dependency - relocate("com.github.stefvanschie.inventoryframework", "xyz.alexcrea.inventoryframework") - - filesMatching("plugin.yml") { - expand( - "version" to "$effectiveVersion-offline", - "libraries" to "" - ) - } - - // Include all project other dependencies - from(project.configurations.runtimeClasspath) - - // Add custom anvil compiled - from(sourceSets.main.get().output) - - dependsOn(processResources) - }) + from(sourceSets.main.get().output) + configurations = listOf(project.configurations.runtimeClasspath.get()) + } // Make the online and offline jar on build named("build") { From d4165df61aabcb313633331846a0b746a657c008 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sat, 10 Jan 2026 19:40:55 +0100 Subject: [PATCH 008/166] try add "on release" workflow --- .github/workflows/gradle.yml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index e2d9ca6..d381eb2 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -12,9 +12,11 @@ on: branches: [ "v1.x.x", "v2.x.x" ] pull_request: branches: [ "v1.x.x", "v2.x.x" ] + release: + types: [published] concurrency: - group: ${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -31,17 +33,17 @@ jobs: java-version: | 21 distribution: 'temurin' - cache: 'gradle' # 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@v4 + 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 @@ -63,7 +65,7 @@ jobs: echo "ONLINE_JAR_NAME=$(basename $ONLINE_JAR_PATH)" >> $GITHUB_ENV echo "OFFLINE_JAR_NAME=$(basename $OFFLINE_JAR_PATH)" >> $GITHUB_ENV - # upload the named jars + # upload the named jars as artifact - name: Upload online JAR artifact uses: actions/upload-artifact@v4 with: @@ -78,4 +80,13 @@ jobs: - name: Summarize tests results uses: jeantessier/test-summary-action@v1 - if: ${{ always() }} \ No newline at end of file + 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: | + ${{ env.ONLINE_JAR_PATH }} + ${{ env.OFFLINE_JAR_PATH }} \ No newline at end of file From 9e0e546367580f16e20824e02b0735b639c654de Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sat, 10 Jan 2026 19:51:45 +0100 Subject: [PATCH 009/166] why did I used path and not name lol --- .github/workflows/gradle.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index d381eb2..e445613 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -88,5 +88,5 @@ jobs: uses: softprops/action-gh-release@v2 with: files: | - ${{ env.ONLINE_JAR_PATH }} - ${{ env.OFFLINE_JAR_PATH }} \ No newline at end of file + build/libs/${{ env.ONLINE_JAR_NAME }} + build/libs/${{ env.OFFLINE_JAR_NAME }} \ No newline at end of file From 5fe65799c85e4b8c6fc61a2fd70a15e97381be9b Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sat, 10 Jan 2026 21:32:13 +0100 Subject: [PATCH 010/166] cache paperweight --- .github/workflows/gradle.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index e445613..e5cdec0 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -33,6 +33,26 @@ jobs: 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 From be3a98078f148d29678839af2598decb160e2347 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 12 Jan 2026 00:43:06 +0100 Subject: [PATCH 011/166] add hangar publish logic --- .github/workflows/gradle.yml | 12 ++++- build.gradle.kts | 100 ++++++++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index e5cdec0..87eb832 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -67,7 +67,7 @@ jobs: run: echo "SMALL_COMMIT_HASH=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV - name: Build with Gradle Wrapper - run: ./gradlew build --parallel + run: ./gradlew build --parallel --stacktrace # only submit dependency on push - name: Generate and submit dependency graph @@ -109,4 +109,12 @@ jobs: with: files: | build/libs/${{ env.ONLINE_JAR_NAME }} - build/libs/${{ env.OFFLINE_JAR_NAME }} \ No newline at end of file + build/libs/${{ env.OFFLINE_JAR_NAME }} + + - name: Hangar release + if: ${{ (github.event_name != 'release' || github.event_name != 'push') && github.repository_owner == 'alexcrea' && success() }} + env: + RELEASE_CHANGELOG: ${{ github.event.release.body }} + IS_GITHUB_PRERELEASE: ${{ github.event.release.prerelease }} + HANGAR_API_TOKEN: ${{ secrets.HANGAR_API_TOKEN }} + run: ./gradlew publishAllPublicationsToHangar --stacktrace diff --git a/build.gradle.kts b/build.gradle.kts index f005e45..b3d4b51 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,7 +2,10 @@ 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 "2.1.0" @@ -15,13 +18,16 @@ plugins { 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.15.9" +val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null +val isPreRelease = System.getenv("IS_PRERELEASE") == "true" val effectiveVersion = "$version" + - (if (System.getenv("SMALL_COMMIT_HASH") != null) "-dev-${System.getenv("SMALL_COMMIT_HASH")!!}" else "") + (if (isDevBuild) "-dev-${System.getenv("SMALL_COMMIT_HASH")!!}" else "") repositories { // EcoEnchants @@ -170,7 +176,7 @@ tasks { "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/**") @@ -319,3 +325,93 @@ publishing { } } } + +// 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 + } + + return changelog +} + +hangarPublish { + + fun HangarPublication.configure(isOnline: Boolean, devChannel: String, releaseChannel: String) { + version.set(effectiveVersion + if(isOnline) "" else "-offline") + 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") + } + +} From 69f0e2936e31dc0ee84e0c32553f85300b33b595 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 12 Jan 2026 00:56:55 +0100 Subject: [PATCH 012/166] forgot paper version --- gradle.properties | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 95311bf..1a8f2e4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,4 +6,8 @@ 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 \ No newline at end of file +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-1.21.11 + From e1c794403cf614e541920049d9c3e639c51e5d6a Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 12 Jan 2026 01:05:15 +0100 Subject: [PATCH 013/166] print log on build for release debug --- build.gradle.kts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index b3d4b51..a22feea 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,13 @@ group = "xyz.alexcrea" version = "1.15.9" val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null -val isPreRelease = System.getenv("IS_PRERELEASE") == "true" +val isPreRelease = System.getenv("IS_GITHUB_PRERELEASE") == "true" + +println("testing env variable") +println(System.getenv("IS_GITHUB_PRERELEASE")) +println(System.getenv("IS_GITHUB_PRERELEASE") == "true") +println(System.getenv("RELEASE_CHANGELOG")) + val effectiveVersion = "$version" + (if (isDevBuild) "-dev-${System.getenv("SMALL_COMMIT_HASH")!!}" else "") From 35c67e4207545eb5733b5759fb57df32856158cd Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 12 Jan 2026 01:17:34 +0100 Subject: [PATCH 014/166] set env variable early --- .github/workflows/gradle.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 87eb832..677d3a3 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -66,6 +66,12 @@ jobs: if: ${{ github.event_name != 'release' && success() }} run: echo "SMALL_COMMIT_HASH=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV + - name: Prepare release env variable + if: ${{ github.event_name != 'release' && success() }} + run: | + echo "RELEASE_CHANGELOG=${{ github.event.release.body }}" >> $GITHUB_ENV + echo "IS_GITHUB_PRERELEASE=${{ github.event.release.prerelease }}" >> $GITHUB_ENV + - name: Build with Gradle Wrapper run: ./gradlew build --parallel --stacktrace @@ -114,7 +120,5 @@ jobs: - name: Hangar release if: ${{ (github.event_name != 'release' || github.event_name != 'push') && github.repository_owner == 'alexcrea' && success() }} env: - RELEASE_CHANGELOG: ${{ github.event.release.body }} - IS_GITHUB_PRERELEASE: ${{ github.event.release.prerelease }} HANGAR_API_TOKEN: ${{ secrets.HANGAR_API_TOKEN }} run: ./gradlew publishAllPublicationsToHangar --stacktrace From 203713385a92409154b6ebfaa016bc349179d631 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 12 Jan 2026 01:27:45 +0100 Subject: [PATCH 015/166] invert bad logic --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 677d3a3..4827530 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -67,7 +67,7 @@ jobs: run: echo "SMALL_COMMIT_HASH=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV - name: Prepare release env variable - if: ${{ github.event_name != 'release' && success() }} + if: ${{ github.event_name == 'release' && success() }} run: | echo "RELEASE_CHANGELOG=${{ github.event.release.body }}" >> $GITHUB_ENV echo "IS_GITHUB_PRERELEASE=${{ github.event.release.prerelease }}" >> $GITHUB_ENV From 4a2a9c5b3a60d0cc7c358f4cb9b6a80054502b02 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 12 Jan 2026 01:33:51 +0100 Subject: [PATCH 016/166] try multiline --- .github/workflows/gradle.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 4827530..abcd91c 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -69,7 +69,9 @@ jobs: - name: Prepare release env variable if: ${{ github.event_name == 'release' && success() }} run: | - echo "RELEASE_CHANGELOG=${{ github.event.release.body }}" >> $GITHUB_ENV + echo "RELEASE_CHANGELOG<> $GITHUB_ENV + echo "${{ github.event.release.body || '' }}" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV echo "IS_GITHUB_PRERELEASE=${{ github.event.release.prerelease }}" >> $GITHUB_ENV - name: Build with Gradle Wrapper From 5c32e819fdc3b143787a0cfcf2faee8aedd6d907 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 12 Jan 2026 01:45:16 +0100 Subject: [PATCH 017/166] pre release specific suffix --- build.gradle.kts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a22feea..4adb5be 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,11 +27,6 @@ version = "1.15.9" val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null val isPreRelease = System.getenv("IS_GITHUB_PRERELEASE") == "true" -println("testing env variable") -println(System.getenv("IS_GITHUB_PRERELEASE")) -println(System.getenv("IS_GITHUB_PRERELEASE") == "true") -println(System.getenv("RELEASE_CHANGELOG")) - val effectiveVersion = "$version" + (if (isDevBuild) "-dev-${System.getenv("SMALL_COMMIT_HASH")!!}" else "") @@ -364,7 +359,11 @@ fun changelog(isOnline: Boolean): String { hangarPublish { fun HangarPublication.configure(isOnline: Boolean, devChannel: String, releaseChannel: String) { - version.set(effectiveVersion + if(isOnline) "" else "-offline") + 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)) From 73fd79b9da4485f9c316bb4c239305ecac0887f2 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 12 Jan 2026 03:13:49 +0100 Subject: [PATCH 018/166] add release discord webhook --- .github/workflows/gradle.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index abcd91c..c8e9c9e 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -124,3 +124,16 @@ jobs: env: HANGAR_API_TOKEN: ${{ secrets.HANGAR_API_TOKEN }} run: ./gradlew publishAllPublicationsToHangar --stacktrace + + + - 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 == false && '' || 'beta' }} version of custom anvil ! <:CustomAnvil:1262550667986342001>([Modrinth](https://modrinth.com/plugin/customanvil), [Hangar](https://hangar.papermc.io/alexcrea/CustomAnvil) links) and [GitHub](${{ github.event.release.html_url }} links + -# note: automated release. spigot and modrinth are not uploaded yet. it is available in hangar and github + + ${{ env.RELEASE_CHANGELOG }} \ No newline at end of file From dc7f3f5e20b83dd0fd2d6b6966e24cdf50d07f1a Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 13 Jan 2026 01:24:43 +0100 Subject: [PATCH 019/166] add modrinth release and fix discord message --- .github/workflows/gradle.yml | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index c8e9c9e..983e622 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -73,6 +73,9 @@ jobs: echo "${{ github.event.release.body || '' }}" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV echo "IS_GITHUB_PRERELEASE=${{ github.event.release.prerelease }}" >> $GITHUB_ENV + + echo "MODRINTH_VERSIONS='["1.18.x", "1.19.x", "1.20.x", "1.21.x"]'" >> $GITHUB_ENV + echo "MODRINTH_PLATFORMS='["spigot", "paper", "purpur", "folia"]'" >> $GITHUB_ENV - name: Build with Gradle Wrapper run: ./gradlew build --parallel --stacktrace @@ -125,6 +128,31 @@ jobs: HANGAR_API_TOKEN: ${{ secrets.HANGAR_API_TOKEN }} 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 }} + + - 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 == false && '' || '-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 }} - name: Send release note to discord if: ${{ github.event_name == 'release' && github.repository_owner == 'alexcrea' && success() }} @@ -133,7 +161,7 @@ jobs: webhook-url: ${{ secrets.RELEASE_WEBHOOK_URL }} content: | ${{ github.event.release.prerelease == false && '<@&1338546156325568642>' || '<@&1352296092989001768>' }} - # New ${{ github.event.release.prerelease == false && '' || 'beta' }} version of custom anvil ! <:CustomAnvil:1262550667986342001>([Modrinth](https://modrinth.com/plugin/customanvil), [Hangar](https://hangar.papermc.io/alexcrea/CustomAnvil) links) and [GitHub](${{ github.event.release.html_url }} links - -# note: automated release. spigot and modrinth are not uploaded yet. it is available in hangar and github + # New ${{ github.event.release.prerelease == false && '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. it is available in hangar and github ${{ env.RELEASE_CHANGELOG }} \ No newline at end of file From 18a0f58e684169e9272651ff8a33a6143679b74b Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 13 Jan 2026 01:51:25 +0100 Subject: [PATCH 020/166] fix modrinth release --- .github/workflows/gradle.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 983e622..5b581d2 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -74,8 +74,8 @@ jobs: echo "EOF" >> $GITHUB_ENV echo "IS_GITHUB_PRERELEASE=${{ github.event.release.prerelease }}" >> $GITHUB_ENV - echo "MODRINTH_VERSIONS='["1.18.x", "1.19.x", "1.20.x", "1.21.x"]'" >> $GITHUB_ENV - echo "MODRINTH_PLATFORMS='["spigot", "paper", "purpur", "folia"]'" >> $GITHUB_ENV + echo MODRINTH_VERSIONS='["1.18.x", "1.19.x", "1.20.x", "1.21.x"]' >> $GITHUB_ENV + echo MODRINTH_PLATFORMS='["spigot", "paper", "purpur", "folia"]' >> $GITHUB_ENV - name: Build with Gradle Wrapper run: ./gradlew build --parallel --stacktrace @@ -140,6 +140,7 @@ jobs: 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() }} @@ -153,6 +154,7 @@ jobs: game-versions: ${{ env.MODRINTH_VERSIONS }} channel: ${{ github.event.release.prerelease == false && 'release' || 'beta' }} files: build/libs/${{ env.ONLINE_JAR_NAME }} + changelog: ${{ env.RELEASE_CHANGELOG }} - name: Send release note to discord if: ${{ github.event_name == 'release' && github.repository_owner == 'alexcrea' && success() }} From 675a16c9b4bcc626880bd51a72e574814d60cb8c Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 13 Jan 2026 02:09:15 +0100 Subject: [PATCH 021/166] make modrinth publish run every time --- .github/workflows/gradle.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 5b581d2..2daf897 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -73,7 +73,9 @@ jobs: echo "${{ github.event.release.body || '' }}" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV echo "IS_GITHUB_PRERELEASE=${{ github.event.release.prerelease }}" >> $GITHUB_ENV - + + - name: Prepare publish env variable + run: | echo MODRINTH_VERSIONS='["1.18.x", "1.19.x", "1.20.x", "1.21.x"]' >> $GITHUB_ENV echo MODRINTH_PLATFORMS='["spigot", "paper", "purpur", "folia"]' >> $GITHUB_ENV From f14fe20faf11d87ceef464d11ed640b47e1d220b Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 13 Jan 2026 02:20:51 +0100 Subject: [PATCH 022/166] change message a bit again --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 2daf897..2af7d05 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -166,6 +166,6 @@ jobs: content: | ${{ github.event.release.prerelease == false && '<@&1338546156325568642>' || '<@&1352296092989001768>' }} # New ${{ github.event.release.prerelease == false && '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. it is available in hangar and github + -# note: automated release. spigot is not uploaded yet. ${{ env.RELEASE_CHANGELOG }} \ No newline at end of file From ea6c5724fa5938339bb768fdc8bd858ed55007ef Mon Sep 17 00:00:00 2001 From: alexcrea Date: Fri, 16 Jan 2026 21:43:34 +0100 Subject: [PATCH 023/166] no changelog for build [skip ci] --- build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 4adb5be..1f597f7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -353,6 +353,10 @@ fun changelog(isOnline: Boolean): String { "You likely want to use the normal version of this plugin\n\n" + changelog } + if(changelog == null || changelog.isEmpty()) { + changelog = "empty changelog" + } + return changelog } From 377bc4c1d8adff518caf626547ec28388c39fe51 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 27 Jan 2026 21:05:03 +0100 Subject: [PATCH 024/166] use correct anvil combine method --- .../plugins/ExcellentEnchantsDependency.kt | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt index 98d80ff..dcd104e 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt @@ -184,11 +184,20 @@ class ExcellentEnchantsDependency { ) this.handleRechargeMethod.setAccessible(true) - this.handleCombineMethod = this.usedAnvilListener.javaClass.getDeclaredMethod( - "handleCombine", - PrepareAnvilEvent::class.java, ItemStack::class.java, ItemStack::class.java, ItemStack::class.java - ) - this.handleCombineMethod.setAccessible(true) + try { + this.usedAnvilListener.javaClass.methods.forEach { method -> CustomAnvil.instance.logger.warning { method.name } } + 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) + } } From b7e19355a84d4139a7234bdcc8221e1d75df74e9 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 27 Jan 2026 21:05:14 +0100 Subject: [PATCH 025/166] version bump --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 1f597f7..90de68e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } group = "xyz.alexcrea" -version = "1.15.9" +version = "1.15.10" val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null val isPreRelease = System.getenv("IS_GITHUB_PRERELEASE") == "true" From 76e5059632e6f957979f7b64e8b805d8d1a1af7c Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 27 Jan 2026 21:28:33 +0100 Subject: [PATCH 026/166] use reflection for enchantment definition --- .../enchant/wrapped/CAEEV5Enchantment.java | 83 +++++++++++++++++-- .../plugins/ExcellentEnchantsDependency.kt | 3 +- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java index e91930f..a31a55c 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java @@ -5,31 +5,32 @@ 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 su.nightexpress.excellentenchants.api.wrapper.EnchantDefinition; 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 EnchantDefinition definition; + @NotNull Object definition; public CAEEV5Enchantment(@NotNull CustomEnchantment enchantment) { - super(enchantment.getBukkitEnchantment(), EnchantmentRarity.getRarity(enchantment.getDefinition().getAnvilCost())); + super(enchantment.getBukkitEnchantment(), EnchantmentRarity.getRarity(getAnvilCost(enchantment))); this.eeenchantment = enchantment; - this.definition = enchantment.getDefinition(); + this.definition = getDefinition(enchantment); } @Override public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull Material itemMat) { - if (!definition.hasConflicts()) return false; + if (!hasConflicts()) return false; - Set conflicts = definition.getExclusiveSet(); + Set conflicts = getExclusiveSet(); for (CAEnchantment caEnchantment : enchantments.keySet()) { if (conflicts.contains(caEnchantment.getName())) return true; @@ -52,4 +53,74 @@ public class CAEEV5Enchantment extends CABukkitEnchantment implements Additional 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/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt index dcd104e..ebc4ad9 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt @@ -163,8 +163,8 @@ class ExcellentEnchantsDependency { } when (listenerVersion) { + ListenerVersion.V5_3, ListenerVersion.V5, - ListenerVersion.V5_3 -> this.usedAnvilListener = v5AnvilListener!! ListenerVersion.PRE_V5 -> this.usedAnvilListener = preV5AnvilListener!! ListenerVersion.LEGACY -> this.usedAnvilListener = legacyAnvilListener!! @@ -185,7 +185,6 @@ class ExcellentEnchantsDependency { this.handleRechargeMethod.setAccessible(true) try { - this.usedAnvilListener.javaClass.methods.forEach { method -> CustomAnvil.instance.logger.warning { method.name } } this.handleCombineMethod = this.usedAnvilListener.javaClass.getDeclaredMethod( "anvilCombine", PrepareAnvilEvent::class.java, ItemStack::class.java, ItemStack::class.java, ItemStack::class.java From 4dd7d6361b4e8b10aa3be346603b7f780748efc5 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Wed, 4 Feb 2026 17:37:39 +0100 Subject: [PATCH 027/166] fix custom anvil not checking enchantment key name version bump --- build.gradle.kts | 2 +- .../xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 90de68e..7597db0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } group = "xyz.alexcrea" -version = "1.15.10" +version = "1.15.11" val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null val isPreRelease = System.getenv("IS_GITHUB_PRERELEASE") == "true" diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java index a31a55c..813eda8 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java @@ -34,6 +34,7 @@ public class CAEEV5Enchantment extends CABukkitEnchantment implements Additional for (CAEnchantment caEnchantment : enchantments.keySet()) { if (conflicts.contains(caEnchantment.getName())) return true; + if (conflicts.contains(caEnchantment.getKey().toString())) return true; } return false; From c8f1aa65a26260d5ce7736c00789aff6e04bf529 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Wed, 4 Feb 2026 17:41:36 +0100 Subject: [PATCH 028/166] change message a bit --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 2af7d05..b5dc6b6 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -165,7 +165,7 @@ jobs: webhook-url: ${{ secrets.RELEASE_WEBHOOK_URL }} content: | ${{ github.event.release.prerelease == false && '<@&1338546156325568642>' || '<@&1352296092989001768>' }} - # New ${{ github.event.release.prerelease == false && '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) + # New ${{ github.event.release.prerelease == false && '' || '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. ${{ env.RELEASE_CHANGELOG }} \ No newline at end of file From 8e3f190bb3c17e635162d3959de8ed2ae266f09f Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 10 Feb 2026 12:14:04 +0100 Subject: [PATCH 029/166] version bump --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7597db0..f690eaa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } group = "xyz.alexcrea" -version = "1.15.11" +version = "1.15.12" val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null val isPreRelease = System.getenv("IS_GITHUB_PRERELEASE") == "true" From 9ed43f3def8fbace465c442135e9a5a9e9f065c6 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 10 Feb 2026 12:14:35 +0100 Subject: [PATCH 030/166] fix eco enchant conflict with everything --- .../xyz/alexcrea/cuanvil/enchant/wrapped/CAEcoEnchant.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEcoEnchant.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEcoEnchant.java index 6e74b73..b0e8fba 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEcoEnchant.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEcoEnchant.java @@ -26,6 +26,10 @@ public class CAEcoEnchant extends CABukkitEnchantment implements AdditionalTestE public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull Material itemMat) { 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; } From 2c30446bc19b75e404ee71558e6e7796fd9b245e Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 10 Feb 2026 12:39:00 +0100 Subject: [PATCH 031/166] don't use eco's pre anvil event player --- .../xyz/alexcrea/cuanvil/dependency/DependencyManager.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt index 9c8c159..42383b6 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt @@ -1,6 +1,5 @@ package xyz.alexcrea.cuanvil.dependency -import com.willfp.eco.core.gui.player import io.delilaheve.CustomAnvil import net.kyori.adventure.text.Component import org.bukkit.Bukkit @@ -300,7 +299,7 @@ object DependencyManager { if (!bypass && (externGuiTester?.testIfGui(event.view) == true)) bypass = true // Test if in an ax player warp rating gui - if (!bypass && (axPlayerWarpsCompatibility?.testIfGui(event.player) == true)) bypass = true + if (!bypass && (axPlayerWarpsCompatibility?.testIfGui(event.view.player) == true)) bypass = true return bypass } From 7aeb776ce03a1e6d4d49c01da80f0f898bc61e50 Mon Sep 17 00:00:00 2001 From: alexd <42614139+alexcrea@users.noreply.github.com> Date: Sun, 22 Feb 2026 00:42:33 +0100 Subject: [PATCH 032/166] add run dir for myself --- .gitignore | 3 +++ .run/Server.run.xml | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 .run/Server.run.xml diff --git a/.gitignore b/.gitignore index e7d8069..982299c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ /impl/*/build /impl/*/.gradle +# run folder +/run/ + # other random folders /htmlReport /.kotlin/errors 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 From ae8167faecaa0ad15a6609e35699364edd4b56de Mon Sep 17 00:00:00 2001 From: alexd <42614139+alexcrea@users.noreply.github.com> Date: Sun, 22 Feb 2026 01:07:14 +0100 Subject: [PATCH 033/166] safer start --- src/main/kotlin/io/delilaheve/CustomAnvil.kt | 102 +++++++++++++++---- 1 file changed, 82 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/io/delilaheve/CustomAnvil.kt b/src/main/kotlin/io/delilaheve/CustomAnvil.kt index c747189..ca25147 100644 --- a/src/main/kotlin/io/delilaheve/CustomAnvil.kt +++ b/src/main/kotlin/io/delilaheve/CustomAnvil.kt @@ -81,12 +81,91 @@ open class CustomAnvil : JavaPlugin() { } + // 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) + if(trySafeStart()) return + } + // Add commands + try { + prepareCommand() + } catch (e: Exception) { + logger.log(Level.SEVERE, "error trying to register commands" , 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) + if(tryDirtyStart()) return + } + + // Load dependency + try { + DependencyManager.loadDependency() + } catch (e: Exception) { + logger.log(Level.SEVERE, "error loading dependency compatibility", e) + if(tryDirtyStart()) return + } + + // Register listeners + try { + registerListeners() + } catch (e: Exception) { + logger.log(Level.SEVERE, "error registering listeners", e) + if(tryDirtyStart()) return + } + + // Load metrics + try { + Metrics(this, bstatsPluginId) + } catch (_: Exception) {} + + // Load other thing later. + // It is so other dependent plugins can implement there event listener before we fire them. + DependencyManager.scheduler.scheduleGlobally(this) { loadEnchantmentSystemDirty() } + } + + private fun loadEnchantmentSystemDirty() { + try { + loadEnchantmentSystem() + } catch (e: Exception) { + logger.log(Level.SEVERE, "error initializing enchantment ssytem", e) + tryDirtyStart() + } + } + + private fun legacyCheck() { // Disable old plugin name if exist val potentialPlugin = Bukkit.getPluginManager().getPlugin("UnsafeEnchantsPlus") if (potentialPlugin != null) { @@ -99,34 +178,17 @@ open class CustomAnvil : JavaPlugin() { logger.warning("It seems you are using spigot") logger.warning("Please take notice that spigot is less supported than paper and derivatives") } + } - // Add commands - prepareCommand() - - // Load chat listener + private fun registerListeners() { + // Register chat listener chatListener = ChatEventListener() server.pluginManager.registerEvents(chatListener, this) - // Load default configuration - if (!ConfigHolder.loadDefaultConfig()) { - logger.log(Level.SEVERE,"could not load default config.") - return - } - - // Load dependency - DependencyManager.loadDependency() - // Register anvil events server.pluginManager.registerEvents(PrepareAnvilListener(), this) server.pluginManager.registerEvents(AnvilResultListener(), this) server.pluginManager.registerEvents(AnvilCloseListener(DependencyManager.packetManager), this) - - // Load metrics - Metrics(this, bstatsPluginId) - - // Load other thing later. - // It is so other dependent plugins can implement there event listener before we fire them. - DependencyManager.scheduler.scheduleGlobally(this, {loadEnchantmentSystem()}) } private fun loadEnchantmentSystem(){ From c57de03442c69c558d528211a9890619a67d7ea3 Mon Sep 17 00:00:00 2001 From: alexd <42614139+alexcrea@users.noreply.github.com> Date: Sun, 22 Feb 2026 04:02:20 +0100 Subject: [PATCH 034/166] add credits move compatibility list & remove spigot link spigot is not recomended anymore as do not have auto upload --- COMPATIBILITY.MD | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ CREDITS.MD | 35 ++++++++++++++++++++++++++++++++ README.md | 39 ++++++------------------------------ 3 files changed, 93 insertions(+), 33 deletions(-) create mode 100644 COMPATIBILITY.MD create mode 100644 CREDITS.MD diff --git a/COMPATIBILITY.MD b/COMPATIBILITY.MD new file mode 100644 index 0000000..2be6f7a --- /dev/null +++ b/COMPATIBILITY.MD @@ -0,0 +1,52 @@ +### 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 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/): + Use ExcellentEnchants item type + +- [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/): + 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/): + Support by Custom Anvil but still experimental. Automatic configuration. Plugin is not actively developed anymore + +#### Anvil Mechanics +- [Disenchantment](https://www.spigotmc.org/resources/disenchantment-1-21-1-1-20-6-new-book-splitting-mechanics.110741/) + 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/) + For bag upgrade and skin via anvil. (version >= 1.31.0) + +- [AxPlayerWarp](https://modrinth.com/project/QDJHDKvi) + For its anvil inventory usage + +- [ToolsStats](https://modrinth.com/project/oBZj9E15) + For token application using anvil + +### Known Partially Incompatible +- [UberEnchant](https://modrinth.com/plugin/uberenchant) + Anvil handling as they are doing something similar to CustomAnvil. +It is by no mean there faults and I recomend checking them out + +- [SuperEnchant](https://modrinth.com/plugin/superenchants) + Reported potential incompatibility + +- [AdvencedEnchantments](https://ae.advancedplugins.net/) + 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..5311480 --- /dev/null +++ b/CREDITS.MD @@ -0,0 +1,35 @@ +**Custom Anvil** is based on [Unsafe Enchants](https://github.com/DelilahEve/UnsafeEnchants) by DelilahEve. + +Thanks for all the contributors of bukkit, spigot, the paper team and the adventure API developers +thanks JetBrain for making IntelliJ + +### Dependencies +Here dependencies 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 + +### Compatibility +Here is to credits all the author of plugins +It partially repeat the the [Compatibility list](https://github.com/alexcrea/CustomAnvil/blob/v1.x.x/COMPATIBILITY.md) +- Big Thanks for H7KZ for [Disenchantment](https://github.com/H7KZ/Disenchantment) +- [Enchantment²](https://www.spigotmc.org/resources/enchants-squared-the-enchantsplus-rewrite-custom-enchantments-that-act-like-vanilla-ones.86747/) by Athlaeos +- [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 +- [ExcellentEnchants](https://www.spigotmc.org/resources/excellentenchants-%E2%AD%90-75-vanilla-like-enchantments.61693/) by NightExpress +- [HavenBags](https://www.spigotmc.org/resources/havenbags-shulker-like-player-bound-bags-1-17-1-21-4.110420/) by hyperdefined +- [AxPlayerWarp](https://modrinth.com/project/QDJHDKvi) by ArtillexStudios +- [ToolsStats](https://modrinth.com/project/oBZj9E15) by Valorless + +### Special Thanks + +Thanks for Microsoft leading me into using a better operating system +Thanks for all the users trying my plugin for these niche use cases +and for 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 + diff --git a/README.md b/README.md index bf119e9..e1d7ec6 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,10 @@ It is expected to work on 1.18 to 1.21.7 minecraft servers running spigot or paper. (the plugin support of 1.16.5 to 1.17.1 is experimental and may encounter issues) -**Custom Anvil** was previously named **Unsafe Enchants+**. -It was renamed because it now affects every anvil aspect and not only unsafe enchants\ -**Custom Anvil** is based on [Unsafe Enchants](https://github.com/DelilahEve/UnsafeEnchants) by DelilahEve. - ### Download Locations: the plugin can be downloaded on -[Spigot](https://www.spigotmc.org/resources/custom-anvil.114884), - [modrinth](https://modrinth.com/plugin/customanvil), + [Modrinth](https://modrinth.com/plugin/customanvil), [Hangar](https://hangar.papermc.io/alexcrea/CustomAnvil) or here [on GitHub](https://github.com/alexcrea/CustomAnvil/releases/latest) @@ -57,32 +52,7 @@ anvilconfigreload or carl: Reload every config of this plugin customanvilconfig or configanvil: open a menu for administrator to edit plugin's config in game ``` ### Supported Plugins -Custom Anvil can be compatible with some custom enchantments and anvil mechanics plugins. - -Here is a list of supported custom enchantment plugins with support status: -- [Enchantment²](https://www.spigotmc.org/resources/enchants-squared-the-enchantsplus-rewrite-custom-enchantments-that-act-like-vanilla-ones.86747/): -Support by Custom Anvil but still experimental. Automatic configuration. - -- [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/): -Support by Custom Anvil but still experimental. Need to use /anvilconfigreload or a server restart to add newly added enchantment. -Use EcoEnchant restriction system by default. - -- [ExcellentEnchants](https://www.spigotmc.org/resources/excellentenchants-%E2%AD%90-75-vanilla-like-enchantments.61693/): -Support by Custom Anvil but still experimental. Use ExcellentEnchants item type. - -- [Superenchants](https://modrinth.com/plugin/superenchants) -support by Superenchants. Use CustomAnvil to combine enchantment in anvil in survival. - -Here is a list of supported anvil mechanic plugins with support status: -- [Disenchantment](https://www.spigotmc.org/resources/disenchantment-1-21-1-1-20-6-new-book-splitting-mechanics.110741/) -support by Custom Anvil but still experimental. Mostly use Custom Anvil basic XP settings. (version >= 6.1.5) - -- [HavenBags](https://www.spigotmc.org/resources/havenbags-shulker-like-player-bound-bags-1-17-1-21-4.110420/) -support by Custom Anvil. Not really enchantment related but CustomAnvil should not impact bag upgrade and skin via anvil. (version >= 1.31.0) - -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 on my free time for free. +See the [Compatibility list](https://github.com/alexcrea/CustomAnvil/blob/v1.x.x/COMPATIBILITY.md) ### Overriding Too Expensive @@ -108,9 +78,12 @@ see [Here](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs) --- Custom anvil [use bstat](https://bstats.org/plugin/bukkit/Unsafe%20Enchants%20Plus/20923) for metric. You can [disable it](https://bstats.org/getting-started) if you like. +### 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 +- Get restriction on unknown enchantments (planned for V2) - More features for custom anvil craft ### Known issue: From 9cf06fbb93299e4609e83d614818e016df1e69a5 Mon Sep 17 00:00:00 2001 From: alexd <42614139+alexcrea@users.noreply.github.com> Date: Sun, 22 Feb 2026 04:15:30 +0100 Subject: [PATCH 035/166] add bstat in credit --- CREDITS.MD | 1 + 1 file changed, 1 insertion(+) diff --git a/CREDITS.MD b/CREDITS.MD index 5311480..8ac2464 100644 --- a/CREDITS.MD +++ b/CREDITS.MD @@ -11,6 +11,7 @@ Here dependencies are used by custom anvil - [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 +- [bstats](https://bstats.org/) for keeping me motivated ### Compatibility Here is to credits all the author of plugins From d801d8524271f466b06a1f3f49d844f4b51a9376 Mon Sep 17 00:00:00 2001 From: alexd <42614139+alexcrea@users.noreply.github.com> Date: Sun, 22 Feb 2026 04:19:07 +0100 Subject: [PATCH 036/166] better formating --- CREDITS.MD | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/CREDITS.MD b/CREDITS.MD index 8ac2464..e4f2533 100644 --- a/CREDITS.MD +++ b/CREDITS.MD @@ -1,7 +1,8 @@ -**Custom Anvil** is based on [Unsafe Enchants](https://github.com/DelilahEve/UnsafeEnchants) by DelilahEve. +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 +Thanks for all the contributors of bukkit, spigot, the paper team and the adventure API developers \ +Thanks JetBrain for making IntelliJ ### Dependencies Here dependencies are used by custom anvil @@ -11,7 +12,7 @@ Here dependencies are used by custom anvil - [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 -- [bstats](https://bstats.org/) for keeping me motivated +- Thanks [bstats](https://bstats.org/) for keeping me motivated ### Compatibility Here is to credits all the author of plugins @@ -26,11 +27,11 @@ It partially repeat the the [Compatibility list](https://github.com/alexcrea/Cus ### Special Thanks -Thanks for Microsoft leading me into using a better operating system +Thanks for Microsoft leading me into using a better operating system \ Thanks for all the users trying my plugin for these niche use cases and for 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 ! +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 From 49abca2ccfee01eabcb425ada89ad91d539c0c92 Mon Sep 17 00:00:00 2001 From: alexd <42614139+alexcrea@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:44:30 +0100 Subject: [PATCH 037/166] update checker --- CREDITS.MD | 1 + .../cuanvil/update/ModrinthUpdateChecker.java | 214 ++++++++++++++++++ src/main/kotlin/io/delilaheve/CustomAnvil.kt | 21 +- 3 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 src/main/java/xyz/alexcrea/cuanvil/update/ModrinthUpdateChecker.java diff --git a/CREDITS.MD b/CREDITS.MD index e4f2533..b4a3ce8 100644 --- a/CREDITS.MD +++ b/CREDITS.MD @@ -13,6 +13,7 @@ Here dependencies are used by custom anvil - [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 +- [ModrinthUpdateChecker](https://github.com/Clickism/ModrinthUpdateChecker) by Clickism and thanks to the modrinth team ### Compatibility Here is to credits all the author of plugins 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/kotlin/io/delilaheve/CustomAnvil.kt b/src/main/kotlin/io/delilaheve/CustomAnvil.kt index ca25147..b48f4a6 100644 --- a/src/main/kotlin/io/delilaheve/CustomAnvil.kt +++ b/src/main/kotlin/io/delilaheve/CustomAnvil.kt @@ -18,6 +18,7 @@ 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.Metrics @@ -31,8 +32,9 @@ import java.util.logging.Level open class CustomAnvil : JavaPlugin() { companion object { - // bstats plugin id + // pluginIDS private const val bstatsPluginId = 20923 + private const val modrinthPluginID = "S75Ueiq9" // Permission string required to use the plugin's features const val affectedByPluginPermission = "ca.affected" @@ -174,10 +176,25 @@ open class CustomAnvil : JavaPlugin() { logger.warning("Please note CustomAnvil is a more recent version of UnsafeEnchantsPlus") } - if(!PlatformUtil.isPaper) { + 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") } + + 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? -> + if(latestver == null || version.contains(latestver)) return@checkVersion + + logger.warning("An update may be available: $latestver") + } } private fun registerListeners() { From 196392e20633233c28a8602113a7f12bd48d84a0 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 10 Feb 2026 14:26:53 +0100 Subject: [PATCH 038/166] workflow invert & simplify logic to put not "" first --- .github/workflows/gradle.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index b5dc6b6..01518f4 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -151,7 +151,7 @@ jobs: token: ${{ secrets.MODRINTH_TOKEN }} project: S75Ueiq9 name: ${{ github.event.release.name }} - version: ${{ github.event.release.tag_name }}${{ github.event.release.prerelease == false && '' || '-pre' }} + 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' }} @@ -165,7 +165,7 @@ jobs: webhook-url: ${{ secrets.RELEASE_WEBHOOK_URL }} content: | ${{ github.event.release.prerelease == false && '<@&1338546156325568642>' || '<@&1352296092989001768>' }} - # New ${{ github.event.release.prerelease == false && '' || '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) + # 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. ${{ env.RELEASE_CHANGELOG }} \ No newline at end of file From 3eb07a8c09ee1fff0afbe60dae0e4a2a175f46ad Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:41:46 +0200 Subject: [PATCH 039/166] prepare generic command --- src/main/kotlin/io/delilaheve/CustomAnvil.kt | 3 + .../alexcrea/cuanvil/command/CASubCommand.kt | 40 ++++++++++ .../cuanvil/command/CustomAnvilCmd.kt | 77 +++++++++++++++++++ .../cuanvil/command/EditConfigExecutor.kt | 17 ++-- .../cuanvil/command/ReloadExecutor.kt | 14 +++- src/main/resources/plugin.yml | 5 ++ 6 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt diff --git a/src/main/kotlin/io/delilaheve/CustomAnvil.kt b/src/main/kotlin/io/delilaheve/CustomAnvil.kt index b48f4a6..7e1d959 100644 --- a/src/main/kotlin/io/delilaheve/CustomAnvil.kt +++ b/src/main/kotlin/io/delilaheve/CustomAnvil.kt @@ -6,6 +6,7 @@ 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.CustomAnvilCmd import xyz.alexcrea.cuanvil.command.EditConfigExecutor import xyz.alexcrea.cuanvil.command.ReloadExecutor import xyz.alexcrea.cuanvil.config.ConfigHolder @@ -290,6 +291,8 @@ open class CustomAnvil : JavaPlugin() { command = getCommand(commandConfigName) command?.setExecutor(EditConfigExecutor()) + + CustomAnvilCmd(this) } } 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..7558127 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt @@ -0,0 +1,40 @@ +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`") + 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(list: MutableList) { + + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt new file mode 100644 index 0000000..5333b94 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt @@ -0,0 +1,77 @@ +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 java.util.ArrayList +import java.util.Arrays + +class CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { + + // Name of the generic command + companion object { + private const val genericCommandName = "customanvil" + } + + private val editConfigCommand = EditConfigExecutor() + private val commands: ImmutableMap + + init { + commands = ImmutableMap.of( + "gui", editConfigCommand, + "reload", ReloadExecutor() + ) + + val self = plugin.getCommand(genericCommandName)!! + self.setExecutor(this) + self.tabCompleter = this + } + + 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? = if(args.isEmpty()) { + editConfigCommand + }else { + commands[args[0].lowercase()] + } + + if(subcmd == null) { + sender.sendMessage("Invalid subcommand. run `$cmdstr help` to see available commands") + return true + } + + val newargs = args.copyOfRange(1, args.size) + return subcmd.executeCommand(sender, cmd, cmdstr, newargs) + } + + override fun onTabComplete( + sender: CommandSender, + cmd: Command, + cmdstr: String, + args: Array + ): MutableList { + val result = ArrayList() + if(args.isEmpty()) { + for (cmd in commands) { + result.add(cmd.key) + } + } else { + val subcmd = commands[args[0].lowercase()] + subcmd?.tabCompleter(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/EditConfigExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/EditConfigExecutor.kt index f90f765..d489db2 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/EditConfigExecutor.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/EditConfigExecutor.kt @@ -2,17 +2,21 @@ package xyz.alexcrea.cuanvil.command import io.delilaheve.CustomAnvil import org.bukkit.command.Command -import org.bukkit.command.CommandExecutor 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 : CommandExecutor { +class EditConfigExecutor: CASubCommand() { - override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { - if (!sender.hasPermission(CustomAnvil.editConfigPermission)) { + 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 } @@ -25,10 +29,13 @@ class EditConfigExecutor : CommandExecutor { return false } - if (sender !is HumanEntity) return false MainConfigGui.getInstance().show(sender) return true } + override fun allowed(sender: CommandSender): Boolean { + return sender.hasPermission(CustomAnvil.editConfigPermission) + } + } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/ReloadExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/ReloadExecutor.kt index dc2fe8e..f3f97a7 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/ReloadExecutor.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/ReloadExecutor.kt @@ -11,9 +11,13 @@ import xyz.alexcrea.cuanvil.dependency.DependencyManager import xyz.alexcrea.cuanvil.gui.config.global.* import xyz.alexcrea.cuanvil.update.UpdateHandler -class ReloadExecutor : CommandExecutor { - override fun onCommand(sender: CommandSender, cmd: Command, cmdstr: String, args: Array): Boolean { - if (!sender.hasPermission(CustomAnvil.commandReloadPermission)) { +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 } @@ -31,6 +35,10 @@ class ReloadExecutor : CommandExecutor { return commandSuccess } + override fun allowed(sender: CommandSender): Boolean { + return sender.hasPermission(CustomAnvil.commandReloadPermission) + } + /** * Execute the command, return true if success or false otherwise */ diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 9449f8b..f90f975 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -10,6 +10,8 @@ authors: [ DelilahEve, alexcrea ] libraries: [${libraries}] commands: + customanvil: + description: Generic command for custom anvil anvilconfigreload: description: Reload every config of this plugin permission: ca.command.reload @@ -36,6 +38,9 @@ permissions: ca.command.reload: default: op description: Allow administrator to reload the plugin's configs + ca.command.debug: + 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 From 5f707c7397bab7b34fa1d35045aa43be35901314 Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:39:57 +0200 Subject: [PATCH 040/166] progress --- src/main/kotlin/io/delilaheve/CustomAnvil.kt | 6 ++ .../cuanvil/command/CustomAnvilCmd.kt | 4 +- .../alexcrea/cuanvil/command/Diagnostic.kt | 77 +++++++++++++++++++ src/main/resources/plugin.yml | 2 +- src/test/resources/plugin.yml | 9 ++- 5 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/command/Diagnostic.kt diff --git a/src/main/kotlin/io/delilaheve/CustomAnvil.kt b/src/main/kotlin/io/delilaheve/CustomAnvil.kt index 7e1d959..e870bf0 100644 --- a/src/main/kotlin/io/delilaheve/CustomAnvil.kt +++ b/src/main/kotlin/io/delilaheve/CustomAnvil.kt @@ -49,9 +49,13 @@ open class CustomAnvil : JavaPlugin() { // 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" @@ -292,6 +296,8 @@ open class CustomAnvil : JavaPlugin() { command = getCommand(commandConfigName) command?.setExecutor(EditConfigExecutor()) + println(getCommand("customanvil")) + println(getCommand("customanvila")) CustomAnvilCmd(this) } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt index 5333b94..5de1c04 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt @@ -22,9 +22,11 @@ class CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { init { commands = ImmutableMap.of( "gui", editConfigCommand, - "reload", ReloadExecutor() + "reload", ReloadExecutor(), + "diagnostic", Diagnostic(), ) + println(plugin.getCommand(genericCommandName)) val self = plugin.getCommand(genericCommandName)!! self.setExecutor(this) self.tabCompleter = this diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/Diagnostic.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/Diagnostic.kt new file mode 100644 index 0000000..9752894 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/Diagnostic.kt @@ -0,0 +1,77 @@ +package xyz.alexcrea.cuanvil.command + +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.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.entity.HumanEntity + +class Diagnostic: CASubCommand() { + + companion object{ + private const val NO_DIAG_PERM = "You do not have permission to diagnostic this server" + } + + 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") + try { + diagnostic(stb) + } catch(e: Exception){ + // TODO append error message to diag + TODO("error not handled yet $e") + } + + stb.append("\n```") + + if (sender is HumanEntity) { + 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(stb: StringBuilder){ + stb.append("Server Info\n"); + stb.append("Plugin Version: ").append(CustomAnvil.instance.description.version).append("\n"); + stb.append("Server Version: ").append(Bukkit.getVersion()).append(" (").append(Bukkit.getName()).append(')').append("\n"); + stb.append("Plugin Enabled: ").append(if(CustomAnvil.instance.isEnabled) "Yes" else "No").append("\n"); + //stb.append("NMS type: ").append(NMSMapper.hasNMS() ? "Yes" : "No").append("\n"); + stb.append("Java Version: ").append(System.getProperty("java.version")).append("\n"); + stb.append("OS: ").append(System.getProperty("os.name")).append(" ") + .append(System.getProperty("os.version")) + .append(System.getProperty("os.arch")) + .append("\n\n"); + stb.append("Architecture: ").append(System.getProperty("os.arch")).append("\n\n"); + + + + } + + +} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index f90f975..03ed5aa 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -38,7 +38,7 @@ permissions: ca.command.reload: default: op description: Allow administrator to reload the plugin's configs - ca.command.debug: + ca.command.diagnostic: default: op description: Get debug information about the plugin and server ca.config.edit: diff --git a/src/test/resources/plugin.yml b/src/test/resources/plugin.yml index c116cce..37fb95a 100644 --- a/src/test/resources/plugin.yml +++ b/src/test/resources/plugin.yml @@ -11,6 +11,8 @@ 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 @@ -37,6 +39,9 @@ permissions: 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 @@ -55,8 +60,7 @@ permissions: default: op description: Allow player to edit lore via paper if enabled (toggleable) - -# soft depend on old name (UnsafeEnchantsPlus), so I can disable it if it is on the same server (old name for this plugin) +# 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 @@ -66,3 +70,4 @@ softdepend: - EcoEnchants - eco - ExcellentEnchants + - HavenBags From 63353c620593f34b9f6ddc52fc42a3a5e7febafe Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 2 Mar 2026 03:24:42 +0100 Subject: [PATCH 041/166] progress on diagnostic command --- src/main/kotlin/io/delilaheve/CustomAnvil.kt | 11 +- .../alexcrea/cuanvil/command/CASubCommand.kt | 6 +- .../cuanvil/command/CustomAnvilCmd.kt | 22 +- .../alexcrea/cuanvil/command/DebugExecutor.kt | 25 +++ .../alexcrea/cuanvil/command/Diagnostic.kt | 77 ------- .../cuanvil/command/DiagnosticExecutor.kt | 195 ++++++++++++++++++ 6 files changed, 243 insertions(+), 93 deletions(-) create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/command/DebugExecutor.kt delete mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/command/Diagnostic.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt diff --git a/src/main/kotlin/io/delilaheve/CustomAnvil.kt b/src/main/kotlin/io/delilaheve/CustomAnvil.kt index e870bf0..902b76d 100644 --- a/src/main/kotlin/io/delilaheve/CustomAnvil.kt +++ b/src/main/kotlin/io/delilaheve/CustomAnvil.kt @@ -68,6 +68,8 @@ open class CustomAnvil : JavaPlugin() { // Chat message listener lateinit var chatListener: ChatEventListener + var latestVer: String? = null + /** * Logging handler */ @@ -195,10 +197,11 @@ open class CustomAnvil : JavaPlugin() { ModrinthUpdateChecker(modrinthPluginID, loader, null) .setFeatured(featured) .setOnError { logger.log(Level.WARNING, "error trying to fetch latest update", it) } - .checkVersion { latestver: String? -> - if(latestver == null || version.contains(latestver)) return@checkVersion + .checkVersion { latestVer: String? -> + CustomAnvil.latestVer = latestVer + if(latestVer == null || version.contains(latestVer)) return@checkVersion - logger.warning("An update may be available: $latestver") + logger.warning("An update may be available: $latestVer") } } @@ -296,8 +299,6 @@ open class CustomAnvil : JavaPlugin() { command = getCommand(commandConfigName) command?.setExecutor(EditConfigExecutor()) - println(getCommand("customanvil")) - println(getCommand("customanvila")) CustomAnvilCmd(this) } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt index 7558127..85c1a58 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt @@ -33,8 +33,10 @@ abstract class CASubCommand: CommandExecutor { return true } - open fun tabCompleter(list: MutableList) { - + open fun tabCompleter( + sender: CommandSender, + args: Array, + list: MutableList) { } } \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt index 5de1c04..421ec7b 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt @@ -17,14 +17,14 @@ class CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { } private val editConfigCommand = EditConfigExecutor() - private val commands: ImmutableMap + private val commands = ImmutableMap.of( + "gui", editConfigCommand, + "reload", ReloadExecutor(), + "diagnostic", DiagnosticExecutor(), + //"debug", DebugExecutor(), + ) init { - commands = ImmutableMap.of( - "gui", editConfigCommand, - "reload", ReloadExecutor(), - "diagnostic", Diagnostic(), - ) println(plugin.getCommand(genericCommandName)) val self = plugin.getCommand(genericCommandName)!! @@ -61,13 +61,17 @@ class CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { args: Array ): MutableList { val result = ArrayList() - if(args.isEmpty()) { + if(args.size < 3) { for (cmd in commands) { result.add(cmd.key) } } else { - val subcmd = commands[args[0].lowercase()] - subcmd?.tabCompleter(result) + val subcmd = commands[args[1].lowercase()] + + if(subcmd != null) { + val newArgs = args.copyOfRange(1, args.size) + subcmd.tabCompleter(sender, newArgs, result) + } } //assumed all provided tab completed string are lowercase diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DebugExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DebugExecutor.kt new file mode 100644 index 0000000..e7cbe9e --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DebugExecutor.kt @@ -0,0 +1,25 @@ +package xyz.alexcrea.cuanvil.command + +import io.delilaheve.CustomAnvil +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import java.util.logging.Level + +class DebugExecutor : CASubCommand() { + + override fun executeCommand( + sender: CommandSender, + cmd: Command, + cmdstr: String, + args: Array + ): Boolean { + CustomAnvil.instance.logger.log(Level.SEVERE, "aaaaaaaaaaaaaaaaaaa"); + return true + } + + override fun tabCompleter(sender: CommandSender, args: Array, list: MutableList) { + //TODO + + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/Diagnostic.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/Diagnostic.kt deleted file mode 100644 index 9752894..0000000 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/Diagnostic.kt +++ /dev/null @@ -1,77 +0,0 @@ -package xyz.alexcrea.cuanvil.command - -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.command.Command -import org.bukkit.command.CommandSender -import org.bukkit.entity.HumanEntity - -class Diagnostic: CASubCommand() { - - companion object{ - private const val NO_DIAG_PERM = "You do not have permission to diagnostic this server" - } - - 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") - try { - diagnostic(stb) - } catch(e: Exception){ - // TODO append error message to diag - TODO("error not handled yet $e") - } - - stb.append("\n```") - - if (sender is HumanEntity) { - 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(stb: StringBuilder){ - stb.append("Server Info\n"); - stb.append("Plugin Version: ").append(CustomAnvil.instance.description.version).append("\n"); - stb.append("Server Version: ").append(Bukkit.getVersion()).append(" (").append(Bukkit.getName()).append(')').append("\n"); - stb.append("Plugin Enabled: ").append(if(CustomAnvil.instance.isEnabled) "Yes" else "No").append("\n"); - //stb.append("NMS type: ").append(NMSMapper.hasNMS() ? "Yes" : "No").append("\n"); - stb.append("Java Version: ").append(System.getProperty("java.version")).append("\n"); - stb.append("OS: ").append(System.getProperty("os.name")).append(" ") - .append(System.getProperty("os.version")) - .append(System.getProperty("os.arch")) - .append("\n\n"); - stb.append("Architecture: ").append(System.getProperty("os.arch")).append("\n\n"); - - - - } - - -} \ No newline at end of file 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..23ab4b9 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt @@ -0,0 +1,195 @@ +package xyz.alexcrea.cuanvil.command + +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.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.entity.HumanEntity +import org.bukkit.event.inventory.PrepareAnvilEvent +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.dependency.packet.versions.PaperPacketManager +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" + } + + enum class DiagParams(val value: String) { + OS_PRIVACY("os_privacy"), + PLUGIN_PRIVACY("plugin_privacy"), + //NO_TEST("no_anvil_test"), + } + + 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) + try { + diagnostic(stb, params) + } catch(e: Exception){ + stb.append("\n\nError happened trying to get diagnostic data:\n") + .append(e.message).append("\n") + .append(e.stackTrace.joinToString("\n")) + } + + stb.append("\n```") + + if (sender is HumanEntity) { + 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(stb: StringBuilder, params: Set){ + stb.append("Server Info\n") + stb.append("Plugin Version: ").append(CustomAnvil.instance.description.version).append("\n") + stb.append("Latest Update: ").append(CustomAnvil.latestVer).append('\n') + stb.append("Server Version: ").append(Bukkit.getVersion()).append(" (").append(Bukkit.getName()).append(')').append("\n") + stb.append("Plugin Enabled: ").append(if(CustomAnvil.instance.isEnabled) "Yes" else "No").append("\n") + stb.append("NMS type: ").append(fetchNMSType()) + if(!params.contains(DiagParams.OS_PRIVACY)) { + stb.append("Java Version: ").append(System.getProperty("java.version")).append("\n") + stb.append("OS: ").append(System.getProperty("os.name")).append(" ") + .append(System.getProperty("os.version")) + .append(System.getProperty("os.arch")) + .append("\n\n") + } + + if(!params.contains(DiagParams.PLUGIN_PRIVACY)) { + pluginListDiag(stb) + } + prepareAnvilListeners(stb) + + stb.append("\n\n") + } + + private fun fetchNMSType(): String { + val packetManager = DependencyManager.packetManager + val packetManagerClass = packetManager.javaClass + + val result: String + if(packetManagerClass == PaperPacketManager::class.java) { + result = "Paper NMS" + } else if(packetManagerClass == ProtocoLibWrapper::class.java) { + result = "Protocolib" + } else if(packetManagerClass == NoPacketManager::class.java) { + result = "None" + } else { + result = "Version Specific" + } + + return "$result ${if(packetManager.canSetInstantBuild) '✅' else '❌'}" + } + + private val Plugin.pluginNameDisplay: String + get() { + return this.name + " v" + this.description.version + } + + private fun pluginListDiag(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("Enabled Plugins: ").append( + enabledPlugins.stream() + .map { plugin -> plugin!!.pluginNameDisplay } + .reduce { a: String?, b: String? -> "$a, $b" }.orElse("None") + ).append("\n") + + stb.append("Disabled Plugins: ").append( + disabledPlugins.stream() + .map { plugin -> plugin!!.pluginNameDisplay } + .reduce { a: String?, b: String? -> "$a, $b" }.orElse("None") + ).append("\n") + } + + 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("Prepare Anvil Listeners: ").append( + if (eventListeners.isEmpty()) "None" else eventListeners.stream() + .map { plugin -> plugin!!.pluginNameDisplay } + .reduce { a: String?, b: String? -> "$a, $b" }.orElse("None") + ).append("\n\n") + } + +} \ No newline at end of file From 63f2f16b9fc6bf2d6a7ef309a6ec922527b0bfb0 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 2 Mar 2026 19:03:33 +0100 Subject: [PATCH 042/166] add anvil simulation test for diag --- .../cuanvil/command/CustomAnvilCmd.kt | 7 +- .../cuanvil/command/DiagnosticExecutor.kt | 159 ++++++++++++++---- .../cuanvil/listener/PrepareAnvilListener.kt | 8 + src/main/resources/plugin.yml | 2 + 4 files changed, 142 insertions(+), 34 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt index 421ec7b..2a46fa8 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt @@ -7,7 +7,6 @@ import org.bukkit.command.CommandExecutor import org.bukkit.command.CommandSender import org.bukkit.command.TabCompleter import java.util.ArrayList -import java.util.Arrays class CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { @@ -61,12 +60,12 @@ class CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { args: Array ): MutableList { val result = ArrayList() - if(args.size < 3) { - for (cmd in commands) { + if(args.size < 2) { + for (cmd in commands) { result.add(cmd.key) } } else { - val subcmd = commands[args[1].lowercase()] + val subcmd = commands[args[0].lowercase()] if(subcmd != null) { val newArgs = args.copyOfRange(1, args.size) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt index 23ab4b9..e30b6fc 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt @@ -1,5 +1,6 @@ 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 @@ -7,16 +8,27 @@ 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.PluginManager 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.dependency.packet.versions.PaperPacketManager +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener import java.util.* import java.util.stream.Collectors @@ -30,7 +42,8 @@ class DiagnosticExecutor: CASubCommand() { enum class DiagParams(val value: String) { OS_PRIVACY("os_privacy"), PLUGIN_PRIVACY("plugin_privacy"), - //NO_TEST("no_anvil_test"), + NO_MERGE_TEST("no_merge_test"); + // TODO enchant list } private fun fetchParameters(args: Array): EnumSet { @@ -76,17 +89,22 @@ class DiagnosticExecutor: CASubCommand() { val stb = StringBuilder("```\n") val params = fetchParameters(args) + var hasError = false try { - diagnostic(stb, params) - } catch(e: Exception){ + 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()) @@ -104,42 +122,71 @@ class DiagnosticExecutor: CASubCommand() { return sender.hasPermission(CustomAnvil.diagnosticPermission) } - fun diagnostic(stb: StringBuilder, params: Set){ + fun diagnostic(sender: CommandSender, stb: StringBuilder, params: Set){ stb.append("Server Info\n") - stb.append("Plugin Version: ").append(CustomAnvil.instance.description.version).append("\n") - stb.append("Latest Update: ").append(CustomAnvil.latestVer).append('\n') - stb.append("Server Version: ").append(Bukkit.getVersion()).append(" (").append(Bukkit.getName()).append(')').append("\n") - stb.append("Plugin Enabled: ").append(if(CustomAnvil.instance.isEnabled) "Yes" else "No").append("\n") - stb.append("NMS type: ").append(fetchNMSType()) + stb.append("\nPlugin Version: ").append(CustomAnvil.instance.description.version) + 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("Java Version: ").append(System.getProperty("java.version")).append("\n") - stb.append("OS: ").append(System.getProperty("os.name")).append(" ") + 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")) - .append("\n\n") } if(!params.contains(DiagParams.PLUGIN_PRIVACY)) { - pluginListDiag(stb) + pluginListDiag(sender, stb) } prepareAnvilListeners(stb) - stb.append("\n\n") + if(!params.contains(DiagParams.NO_MERGE_TEST)){ + if(sender is Player) { + testMerge(sender, 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 fun fetchNMSType(): String { val packetManager = DependencyManager.packetManager val packetManagerClass = packetManager.javaClass - val result: String - if(packetManagerClass == PaperPacketManager::class.java) { - result = "Paper NMS" - } else if(packetManagerClass == ProtocoLibWrapper::class.java) { - result = "Protocolib" - } else if(packetManagerClass == NoPacketManager::class.java) { - result = "None" - } else { - result = "Version Specific" + val result = when (packetManagerClass) { + PaperPacketManager::class.java -> "Paper NMS" + ProtocoLibWrapper::class.java -> "Protocolib" + NoPacketManager::class.java -> "None" + else -> "Version Specific" + } return "$result ${if(packetManager.canSetInstantBuild) '✅' else '❌'}" @@ -150,7 +197,7 @@ class DiagnosticExecutor: CASubCommand() { return this.name + " v" + this.description.version } - private fun pluginListDiag(stb: StringBuilder) { + private fun pluginListDiag(sender: CommandSender, stb: StringBuilder) { val enabledPlugins: MutableList = ArrayList() val disabledPlugins: MutableList = ArrayList() for (plugin in Bukkit.getPluginManager().plugins) { @@ -161,17 +208,17 @@ class DiagnosticExecutor: CASubCommand() { } } - stb.append("Enabled Plugins: ").append( + stb.append("\nEnabled Plugins: ").append( enabledPlugins.stream() .map { plugin -> plugin!!.pluginNameDisplay } .reduce { a: String?, b: String? -> "$a, $b" }.orElse("None") - ).append("\n") + ) - stb.append("Disabled Plugins: ").append( + stb.append("\nDisabled Plugins: ").append( disabledPlugins.stream() .map { plugin -> plugin!!.pluginNameDisplay } .reduce { a: String?, b: String? -> "$a, $b" }.orElse("None") - ).append("\n") + ) } fun prepareAnvilListeners(stb: StringBuilder) { @@ -185,11 +232,63 @@ class DiagnosticExecutor: CASubCommand() { .collect(Collectors.toSet()) eventListeners.remove(CustomAnvil.instance) - stb.append("Prepare Anvil Listeners: ").append( + 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") - ).append("\n\n") + ) + } + + 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() } } \ 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 index ebf16ed..8d55f3c 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -38,6 +38,8 @@ class PrepareAnvilListener : Listener { const val ANVIL_INPUT_LEFT = 0 const val ANVIL_INPUT_RIGHT = 1 const val ANVIL_OUTPUT_SLOT = 2 + + var IS_EMPTY_TEST = false } private fun ItemStack?.isAir(): Boolean { @@ -63,6 +65,12 @@ class PrepareAnvilListener : Listener { val first = inventory.getItem(ANVIL_INPUT_LEFT) ?: return val second = inventory.getItem(ANVIL_INPUT_RIGHT) + if(IS_EMPTY_TEST) { + event.result = null + IS_EMPTY_TEST = false + return + } + if (isImmutable(first) || isImmutable(second)) { CustomAnvil.verboseLog("Skipping anvil process as one of the two item is immutable") diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 03ed5aa..5807f8d 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -12,6 +12,8 @@ libraries: [${libraries}] commands: customanvil: description: Generic command for custom anvil + aliases: + - ca anvilconfigreload: description: Reload every config of this plugin permission: ca.command.reload From 20509faed4a8002119da2916e02debd079965343 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 2 Mar 2026 19:44:24 +0100 Subject: [PATCH 043/166] add enchantment data --- .../cuanvil/command/DiagnosticExecutor.kt | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt index e30b6fc..9459444 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt @@ -22,12 +22,12 @@ 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.PluginManager 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.dependency.packet.versions.PaperPacketManager +import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener import java.util.* import java.util.stream.Collectors @@ -42,8 +42,8 @@ class DiagnosticExecutor: CASubCommand() { enum class DiagParams(val value: String) { OS_PRIVACY("os_privacy"), PLUGIN_PRIVACY("plugin_privacy"), - NO_MERGE_TEST("no_merge_test"); - // TODO enchant list + NO_MERGE_TEST("no_merge_test"), + FULL_ENCHANTMENT_DATA("full_enchantment_data"), } private fun fetchParameters(args: Array): EnumSet { @@ -142,9 +142,13 @@ class DiagnosticExecutor: CASubCommand() { prepareAnvilListeners(stb) if(!params.contains(DiagParams.NO_MERGE_TEST)){ - if(sender is Player) { - testMerge(sender, stb) - } + if(sender is Player) testMerge(sender, stb) + } + + stb.append("\n\nEnchantments data:") + partialEnchantmentData(stb) + if(params.contains(DiagParams.FULL_ENCHANTMENT_DATA)){ + fullEnchantmentData(stb) } } @@ -291,4 +295,26 @@ class DiagnosticExecutor: CASubCommand() { 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() + }") + + } + } \ No newline at end of file From fbc862a5a37ed2d2a2d4ad8b3d2eb6d84786374c Mon Sep 17 00:00:00 2001 From: alexcrea Date: Fri, 16 Jan 2026 23:58:13 +0100 Subject: [PATCH 044/166] try add conflict after level --- .../alexcrea/cuanvil/api/ConflictBuilder.java | 1 + .../cuanvil/group/EnchantConflictGroup.kt | 44 ++++++++++++++++--- .../cuanvil/group/EnchantConflictManager.kt | 25 ++++++++++- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/ConflictBuilder.java b/src/main/java/xyz/alexcrea/cuanvil/api/ConflictBuilder.java index f662140..1460766 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/api/ConflictBuilder.java +++ b/src/main/java/xyz/alexcrea/cuanvil/api/ConflictBuilder.java @@ -13,6 +13,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +//TODO add conflict after level /** * A Builder for material conflict. */ diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt index 56e923f..dbe8a09 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt @@ -7,10 +7,11 @@ import xyz.alexcrea.cuanvil.enchant.CAEnchantment class EnchantConflictGroup( val name: String, private val cantConflict: AbstractMaterialGroup, - var minBeforeBlock: Int + var minBeforeBlock: Int, ) { private val enchantments = HashSet() + private val conflictAfterLevel = HashMap() fun addEnchantment(enchant: CAEnchantment) { enchantments.add(enchant) @@ -19,19 +20,37 @@ class EnchantConflictGroup( enchantments.addAll(enchants) } - fun allowed(enchants: Set, mat: Material): Boolean { + private fun canBypassConflictByLevel(enchants: Map): Boolean { + // Either there no "conflict after" + if(conflictAfterLevel.isEmpty()) return false + + // Or we check if any conflict after enchantment is true + for (entry in conflictAfterLevel) { + if(enchants.getOrDefault(entry.key, 0) >= entry.value) + return false + } + + return true + } + + fun allowed(enchants: Map, mat: Material): Boolean { if (enchantments.size < minBeforeBlock) { CustomAnvil.verboseLog("Conflicting bc of to many enchantments") return true } - if (cantConflict.contain(mat)) { + 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 (enchantment in enchants) { + 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) { @@ -56,6 +75,21 @@ class EnchantConflictGroup( enchantments.addAll(enchants) } + fun getConflictAfters(): HashMap { + return conflictAfterLevel + } + + fun putConflictAfterLevel(enchantment: CAEnchantment, level: Int): Boolean { + return null != ( + if(level < 0) conflictAfterLevel.remove(enchantment) + else conflictAfterLevel.put(enchantment, level)) + } + + fun setConflictAfterLevel(conflictAfterLevel: HashMap) { + this.conflictAfterLevel.clear() + this.conflictAfterLevel.putAll(conflictAfterLevel) + } + fun getRepresentativeMaterial(): Material { val groups = getCantConflictGroup().getGroups() val groupIterator = groups.iterator() diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt index 169d9e9..5c07e77 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt @@ -16,6 +16,10 @@ class EnchantConflictManager { // Path for the enchantments list const val ENCH_LIST_PATH = "enchantments" + // Path for the enchantments list + //TODO add test and gui + const val AFTER_LEVEL_LIST_PATH = "conflict_after_level" + // Path for group list related to the conflict const val CONFLICT_GROUP_PATH = "notAffectedGroups" @@ -110,6 +114,25 @@ class EnchantConflictManager { } } + val conflictAfterLevels = section.getConfigurationSection(AFTER_LEVEL_LIST_PATH) + if(conflictAfterLevels != null) { + for (enchantName in conflictAfterLevels.getKeys(false)) { + val enchants = getEnchantByIdentifier(enchantName) + if (enchants.isEmpty()) { + CustomAnvil.instance.logger.warning("Enchantment $enchantName do not exist but was asked for conflict after level for conflict $conflictName") + continue + } + + val value = conflictAfterLevels.getInt(enchantName, -1) + if(value < 0) continue + + for (enchant in enchants) { + val previous = conflict.getConflictAfters().getOrDefault(enchant, value) + conflict.putConflictAfterLevel(enchant, value.coerceAtMost(previous)) + } + } + } + return conflict } @@ -187,7 +210,7 @@ class EnchantConflictManager { continue } - val allowed = conflict.allowed(appliedEnchants.keys, mat) + val allowed = conflict.allowed(appliedEnchants, mat) CustomAnvil.verboseLog("Was against $conflict and conflicting: ${!allowed} ") if (!allowed) { if (conflict.getEnchants().size <= 1) { From 33a86cd3bc02bc6cf631a7ac284dc8cb51f0eefe Mon Sep 17 00:00:00 2001 From: alexcrea Date: Fri, 16 Jan 2026 23:59:34 +0100 Subject: [PATCH 045/166] version bump --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index f690eaa..5eb4561 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } group = "xyz.alexcrea" -version = "1.15.12" +version = "1.16.0" val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null val isPreRelease = System.getenv("IS_GITHUB_PRERELEASE") == "true" From 2cff7bd83cc072a29156b402c51843810124aecb Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sat, 17 Jan 2026 00:47:42 +0100 Subject: [PATCH 046/166] made combine with logic work --- .../io/delilaheve/util/EnchantmentUtil.kt | 124 ++++++++++-------- .../cuanvil/group/EnchantConflictGroup.kt | 3 +- 2 files changed, 68 insertions(+), 59 deletions(-) diff --git a/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt b/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt index bee9bda..6a22489 100644 --- a/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt +++ b/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt @@ -4,7 +4,6 @@ import io.delilaheve.CustomAnvil import org.bukkit.entity.HumanEntity import org.bukkit.inventory.ItemStack import xyz.alexcrea.cuanvil.config.ConfigHolder -import xyz.alexcrea.cuanvil.dependency.DependencyManager import xyz.alexcrea.cuanvil.enchant.CAEnchantment import xyz.alexcrea.cuanvil.group.ConflictType import kotlin.math.max @@ -37,81 +36,90 @@ object EnchantmentUtil { var maxEnchantCount = ConfigOptions.getEnchantCountLimit(item.type) if(maxEnchantCount == null || maxEnchantCount < 0) maxEnchantCount = Int.MAX_VALUE - other.forEach { (enchantment, level) -> - if(!enchantment.isAllowed(player)) return@forEach + val allowed = other.filter { (enchantment, _) -> enchantment.isAllowed(player) } + val new = allowed.filter{ (enchantment, _) -> !containsKey(enchantment)} + val old = allowed.filter{ (enchantment, _) -> containsKey(enchantment)} - // Get max level or 255 if player can bypass - val maxLevel = if (bypassLevel) { 255 } + fun maxLevel(enchantment: CAEnchantment): Int { + val max = if (bypassLevel) { 255 } else { ConfigOptions.enchantLimit(enchantment) } - CustomAnvil.verboseLog("Max level of ${enchantment.key} is $maxLevel (bypassLevel is $bypassLevel)") + 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) - // Enchantment not yet in result list - if (!containsKey(enchantment)) { - // 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) - } + 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 + // ... and they're the same level else { - val oldLevel = this[enchantment]!! // <- should not be null. (enchantment already in result list) - - 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){ + // 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( - "Enchantment already in result list, and they are conflicting (${enchantment.key}, conflict: $conflictType)") + "Reached max merge before disable for ${enchantment.key}: $oldLevel/$maxBeforeDisabled)") return@forEach } } - // ... 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) + // Now we increase the enchantment level by 1 + var newLevel = oldLevel + 1 + newLevel = max(min(newLevel, maxLevel), oldLevel) + this[enchantment] = newLevel + } - } - // ... 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 - } - } - - // 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/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt index dbe8a09..ef8a524 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt @@ -26,7 +26,8 @@ class EnchantConflictGroup( // Or we check if any conflict after enchantment is true for (entry in conflictAfterLevel) { - if(enchants.getOrDefault(entry.key, 0) >= entry.value) + val current = enchants.getOrDefault(entry.key, 0) + if(current > entry.value) return false } From a66206a52c208635c55dfb31cfd0b46301ac8a4f Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sat, 17 Jan 2026 01:13:00 +0100 Subject: [PATCH 047/166] add conflict before level --- .../cuanvil/group/EnchantConflictGroup.kt | 36 ++++++++++++++++++- .../cuanvil/group/EnchantConflictManager.kt | 30 ++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt index ef8a524..fd1ea8c 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt @@ -12,6 +12,7 @@ class EnchantConflictGroup( private val enchantments = HashSet() private val conflictAfterLevel = HashMap() + private val conflictBeforeLevel = HashMap() fun addEnchantment(enchant: CAEnchantment) { enchantments.add(enchant) @@ -20,7 +21,7 @@ class EnchantConflictGroup( enchantments.addAll(enchants) } - private fun canBypassConflictByLevel(enchants: Map): Boolean { + private fun canBypassByBeforeLevel(enchants: Map): Boolean { // Either there no "conflict after" if(conflictAfterLevel.isEmpty()) return false @@ -34,6 +35,24 @@ class EnchantConflictGroup( return true } + private fun canBypassByAfterLevel(enchants: Map): Boolean { + // Either there no "conflict after" + if(conflictAfterLevel.isEmpty()) return false + + // Or we check if any conflict after enchantment is true + for (entry in conflictBeforeLevel) { + 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: Material): Boolean { if (enchantments.size < minBeforeBlock) { CustomAnvil.verboseLog("Conflicting bc of to many enchantments") @@ -91,6 +110,21 @@ class EnchantConflictGroup( this.conflictAfterLevel.putAll(conflictAfterLevel) } + fun getConflictBefores(): HashMap { + return conflictBeforeLevel + } + + fun putConflictBeforeLevel(enchantment: CAEnchantment, level: Int): Boolean { + return null != ( + if(level < 0) conflictBeforeLevel.remove(enchantment) + else conflictBeforeLevel.put(enchantment, level)) + } + + fun setConflictBeforeLevel(conflictBeforeLevel: HashMap) { + this.conflictBeforeLevel.clear() + this.conflictBeforeLevel.putAll(conflictBeforeLevel) + } + fun getRepresentativeMaterial(): Material { val groups = getCantConflictGroup().getGroups() val groupIterator = groups.iterator() diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt index 5c07e77..6782803 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt @@ -16,9 +16,13 @@ class EnchantConflictManager { // Path for the enchantments list const val ENCH_LIST_PATH = "enchantments" - // Path for the enchantments list + // Path for list of enchantments conflicting before level //TODO add test and gui - const val AFTER_LEVEL_LIST_PATH = "conflict_after_level" + 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" @@ -114,7 +118,8 @@ class EnchantConflictManager { } } - val conflictAfterLevels = section.getConfigurationSection(AFTER_LEVEL_LIST_PATH) + //TODO find a way to dry this two ? + val conflictAfterLevels = section.getConfigurationSection(CONFLICT_AFTER_LEVEL_LIST_PATH) if(conflictAfterLevels != null) { for (enchantName in conflictAfterLevels.getKeys(false)) { val enchants = getEnchantByIdentifier(enchantName) @@ -133,6 +138,25 @@ class EnchantConflictManager { } } + val conflictBeforeLevels = section.getConfigurationSection(CONFLICT_BEFORE_LEVEL_LIST_PATH) + if(conflictBeforeLevels != null) { + for (enchantName in conflictBeforeLevels.getKeys(false)) { + val enchants = getEnchantByIdentifier(enchantName) + if (enchants.isEmpty()) { + CustomAnvil.instance.logger.warning("Enchantment $enchantName do not exist but was asked for conflict after level for conflict $conflictName") + continue + } + + val value = conflictBeforeLevels.getInt(enchantName, -1) + if(value < 0) continue + + for (enchant in enchants) { + val previous = conflict.getConflictBefores().getOrDefault(enchant, value) + conflict.putConflictBeforeLevel(enchant, value.coerceAtMost(previous)) + } + } + } + return conflict } From 4e15aab024537b5f1c47163409d03b5260f20f29 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sat, 17 Jan 2026 01:21:30 +0100 Subject: [PATCH 048/166] fix minor error --- .../kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt index fd1ea8c..dda6d49 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt @@ -37,7 +37,7 @@ class EnchantConflictGroup( private fun canBypassByAfterLevel(enchants: Map): Boolean { // Either there no "conflict after" - if(conflictAfterLevel.isEmpty()) return false + if(conflictBeforeLevel.isEmpty()) return false // Or we check if any conflict after enchantment is true for (entry in conflictBeforeLevel) { From ec2384bc7f7618f675c1d1e84995ebd129c2f336 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 19 Jan 2026 21:15:25 +0100 Subject: [PATCH 049/166] fix minor feature explaination issue on config --- defaultconfigs/1.18/config.yml | 2 +- defaultconfigs/1.21.11/config.yml | 2 +- defaultconfigs/1.21.9/config.yml | 2 +- defaultconfigs/1.21/config.yml | 2 +- src/main/resources/config.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/defaultconfigs/1.18/config.yml b/defaultconfigs/1.18/config.yml index 877cee9..0b543cd 100644 --- a/defaultconfigs/1.18/config.yml +++ b/defaultconfigs/1.18/config.yml @@ -267,7 +267,7 @@ enchant_values: # 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) + # 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 diff --git a/defaultconfigs/1.21.11/config.yml b/defaultconfigs/1.21.11/config.yml index 10fe8af..9a8eb9c 100644 --- a/defaultconfigs/1.21.11/config.yml +++ b/defaultconfigs/1.21.11/config.yml @@ -285,7 +285,7 @@ enchant_values: # 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) + # 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 diff --git a/defaultconfigs/1.21.9/config.yml b/defaultconfigs/1.21.9/config.yml index cf9460e..809bab9 100644 --- a/defaultconfigs/1.21.9/config.yml +++ b/defaultconfigs/1.21.9/config.yml @@ -279,7 +279,7 @@ enchant_values: # 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) + # 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 diff --git a/defaultconfigs/1.21/config.yml b/defaultconfigs/1.21/config.yml index d7807e9..88eff1a 100644 --- a/defaultconfigs/1.21/config.yml +++ b/defaultconfigs/1.21/config.yml @@ -267,7 +267,7 @@ enchant_values: # 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) + # 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 diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 5476ec7..c1fe214 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -269,7 +269,7 @@ enchant_values: # 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) + # 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 From 39db70d7adb97e149fb13733618839ae256650e4 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Thu, 29 Jan 2026 18:49:05 +0100 Subject: [PATCH 050/166] some more logs --- src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt | 1 + .../alexcrea/cuanvil/listener/PrepareAnvilListener.kt | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt b/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt index 6a22489..b0fcc97 100644 --- a/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt +++ b/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt @@ -30,6 +30,7 @@ object EnchantmentUtil { ) = mutableMapOf().apply { putAll(this@combineWith) + CustomAnvil.verboseLog("Testing merge") val bypassFuse = player.hasPermission(CustomAnvil.bypassFusePermission) val bypassLevel = player.hasPermission(CustomAnvil.bypassLevelPermission) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index 8d55f3c..0c4d2e7 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -71,6 +71,11 @@ class PrepareAnvilListener : Listener { 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") @@ -91,7 +96,9 @@ class PrepareAnvilListener : Listener { if (testCustomRecipe(event, inventory, player, first, second)) return // Test rename lonely item - if (second.isAir()) { + val isAir = second.isAir() + CustomAnvil.verboseLog("checking air in main logic: $isAir") + if (isAir) { doRenaming(event, inventory, player, first) return } From 5ff096190f4920db9779efd4078e2d1786c138e9 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 2 Mar 2026 20:26:38 +0100 Subject: [PATCH 051/166] code cleanup --- .../cuanvil/group/EnchantConflictGroup.kt | 40 ++++++------ .../cuanvil/group/EnchantConflictManager.kt | 63 ++++++++----------- 2 files changed, 46 insertions(+), 57 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt index dda6d49..403d630 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt @@ -11,8 +11,8 @@ class EnchantConflictGroup( ) { private val enchantments = HashSet() - private val conflictAfterLevel = HashMap() - private val conflictBeforeLevel = HashMap() + private val conflictsAfterLevel = HashMap() + private val conflictsBeforeLevel = HashMap() fun addEnchantment(enchant: CAEnchantment) { enchantments.add(enchant) @@ -23,10 +23,10 @@ class EnchantConflictGroup( private fun canBypassByBeforeLevel(enchants: Map): Boolean { // Either there no "conflict after" - if(conflictAfterLevel.isEmpty()) return false + if(conflictsAfterLevel.isEmpty()) return false // Or we check if any conflict after enchantment is true - for (entry in conflictAfterLevel) { + for (entry in conflictsAfterLevel) { val current = enchants.getOrDefault(entry.key, 0) if(current > entry.value) return false @@ -37,10 +37,10 @@ class EnchantConflictGroup( private fun canBypassByAfterLevel(enchants: Map): Boolean { // Either there no "conflict after" - if(conflictBeforeLevel.isEmpty()) return false + if(conflictsBeforeLevel.isEmpty()) return false // Or we check if any conflict after enchantment is true - for (entry in conflictBeforeLevel) { + for (entry in conflictsBeforeLevel) { val current = enchants.getOrDefault(entry.key, 0) if(current < entry.value) return false @@ -96,33 +96,33 @@ class EnchantConflictGroup( } fun getConflictAfters(): HashMap { - return conflictAfterLevel + return conflictsAfterLevel } fun putConflictAfterLevel(enchantment: CAEnchantment, level: Int): Boolean { return null != ( - if(level < 0) conflictAfterLevel.remove(enchantment) - else conflictAfterLevel.put(enchantment, level)) + if(level < 0) conflictsAfterLevel.remove(enchantment) + else conflictsAfterLevel.put(enchantment, level)) } - fun setConflictAfterLevel(conflictAfterLevel: HashMap) { - this.conflictAfterLevel.clear() - this.conflictAfterLevel.putAll(conflictAfterLevel) + fun setConflictsAfterLevel(conflictAfterLevel: HashMap) { + this.conflictsAfterLevel.clear() + this.conflictsAfterLevel.putAll(conflictAfterLevel) } - fun getConflictBefores(): HashMap { - return conflictBeforeLevel + fun getConflictsBefore(): HashMap { + return conflictsBeforeLevel } - fun putConflictBeforeLevel(enchantment: CAEnchantment, level: Int): Boolean { + fun putConflictsBeforeLevel(enchantment: CAEnchantment, level: Int): Boolean { return null != ( - if(level < 0) conflictBeforeLevel.remove(enchantment) - else conflictBeforeLevel.put(enchantment, level)) + if(level < 0) conflictsBeforeLevel.remove(enchantment) + else conflictsBeforeLevel.put(enchantment, level)) } - fun setConflictBeforeLevel(conflictBeforeLevel: HashMap) { - this.conflictBeforeLevel.clear() - this.conflictBeforeLevel.putAll(conflictBeforeLevel) + fun setConflictsBeforeLevel(conflictBeforeLevel: HashMap) { + this.conflictsBeforeLevel.clear() + this.conflictsBeforeLevel.putAll(conflictBeforeLevel) } fun getRepresentativeMaterial(): Material { diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt index 6782803..f710f76 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt @@ -9,6 +9,7 @@ import xyz.alexcrea.cuanvil.enchant.AdditionalTestEnchantment import xyz.alexcrea.cuanvil.enchant.CAEnchantment import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry import java.util.* +import kotlin.collections.set class EnchantConflictManager { @@ -118,48 +119,36 @@ class EnchantConflictManager { } } - //TODO find a way to dry this two ? - val conflictAfterLevels = section.getConfigurationSection(CONFLICT_AFTER_LEVEL_LIST_PATH) - if(conflictAfterLevels != null) { - for (enchantName in conflictAfterLevels.getKeys(false)) { - val enchants = getEnchantByIdentifier(enchantName) - if (enchants.isEmpty()) { - CustomAnvil.instance.logger.warning("Enchantment $enchantName do not exist but was asked for conflict after level for conflict $conflictName") - continue - } + val conflictsAfterLevel = section.getConfigurationSection(CONFLICT_AFTER_LEVEL_LIST_PATH) + val conflictsAfterMap = conflict.getConflictAfters() + fetchConditionalRestriction(conflictsAfterMap, conflictsAfterLevel, conflictName) - val value = conflictAfterLevels.getInt(enchantName, -1) - if(value < 0) continue - - for (enchant in enchants) { - val previous = conflict.getConflictAfters().getOrDefault(enchant, value) - conflict.putConflictAfterLevel(enchant, value.coerceAtMost(previous)) - } - } - } - - val conflictBeforeLevels = section.getConfigurationSection(CONFLICT_BEFORE_LEVEL_LIST_PATH) - if(conflictBeforeLevels != null) { - for (enchantName in conflictBeforeLevels.getKeys(false)) { - val enchants = getEnchantByIdentifier(enchantName) - if (enchants.isEmpty()) { - CustomAnvil.instance.logger.warning("Enchantment $enchantName do not exist but was asked for conflict after level for conflict $conflictName") - continue - } - - val value = conflictBeforeLevels.getInt(enchantName, -1) - if(value < 0) continue - - for (enchant in enchants) { - val previous = conflict.getConflictBefores().getOrDefault(enchant, value) - conflict.putConflictBeforeLevel(enchant, value.coerceAtMost(previous)) - } - } - } + 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) { From d037263e3ffd7918ab8949b5b6c1705ad9dcc6ae Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 2 Mar 2026 20:42:50 +0100 Subject: [PATCH 052/166] add fast stats as a dependency --- CREDITS.MD | 1 + build.gradle.kts | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CREDITS.MD b/CREDITS.MD index b4a3ce8..528ad15 100644 --- a/CREDITS.MD +++ b/CREDITS.MD @@ -13,6 +13,7 @@ Here dependencies are used by custom anvil - [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 diff --git a/build.gradle.kts b/build.gradle.kts index 5eb4561..e1448f1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,6 +36,12 @@ repositories { // ExcellentEnchants maven(url = "https://repo.nightexpressdev.com/releases") + + // for fast stats + maven { + name = "thenextlvlReleases" + url = uri("https://repo.thenextlvl.net/releases") + } } val reobfNMS = providers.gradleProperty("subprojects.reobfnms") @@ -45,6 +51,9 @@ dependencies { // Spigot api compileOnly("org.spigotmc:spigot-api:1.18-R0.1-SNAPSHOT") + // fast stats + implementation("dev.faststats.metrics:bukkit:0.16.0") + // minimessage implementation("net.kyori:adventure-text-minimessage:4.25.0") @@ -132,7 +141,7 @@ allprojects { // Set target version tasks.withType().configureEach { sourceCompatibility = - "16" // We aim for java 16 for minecraft 1.16.5. even if it not really suported by custom anvil. + "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" @@ -156,7 +165,8 @@ tasks { archiveFileName.set(name) // Shadow necessary dependency - relocate("com.github.stefvanschie.inventoryframework", "xyz.alexcrea.inventoryframework") + relocate("com.github.stefvanschie.inventoryframework", "xyz.alexcrea.customanvil.inventoryframework") + relocate("dev.faststats", "xyz.alexcrea.customanvil.faststats") filesMatching("plugin.yml") { expand( From 3e68af06ea06320e598e198eac993cb7f963f091 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 2 Mar 2026 21:58:09 +0100 Subject: [PATCH 053/166] progress on new metrics --- defaultconfigs/1.18/config.yml | 8 ++ defaultconfigs/1.21.11/config.yml | 8 ++ defaultconfigs/1.21.9/config.yml | 8 ++ defaultconfigs/1.21/config.yml | 8 ++ src/main/kotlin/io/delilaheve/CustomAnvil.kt | 10 ++- .../io/delilaheve/util/ConfigOptions.kt | 2 + .../cuanvil/command/CustomAnvilCmd.kt | 1 - .../alexcrea/cuanvil/command/DebugExecutor.kt | 25 ------- .../cuanvil/command/DiagnosticExecutor.kt | 35 +++++---- .../xyz/alexcrea/cuanvil/util/MetricsUtil.kt | 73 +++++++++++++++++++ src/main/resources/config.yml | 10 ++- 11 files changed, 141 insertions(+), 47 deletions(-) delete mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/command/DebugExecutor.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt diff --git a/defaultconfigs/1.18/config.yml b/defaultconfigs/1.18/config.yml index 0b543cd..69a0e18 100644 --- a/defaultconfigs/1.18/config.yml +++ b/defaultconfigs/1.18/config.yml @@ -3,6 +3,14 @@ # 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 +metric_type: auto + # All anvil cost will be capped to limit_repair_value if enabled. # # In other words: diff --git a/defaultconfigs/1.21.11/config.yml b/defaultconfigs/1.21.11/config.yml index 9a8eb9c..c281de8 100644 --- a/defaultconfigs/1.21.11/config.yml +++ b/defaultconfigs/1.21.11/config.yml @@ -3,6 +3,14 @@ # 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 +metric_type: auto + # All anvil cost will be capped to limit_repair_value if enabled. # # In other words: diff --git a/defaultconfigs/1.21.9/config.yml b/defaultconfigs/1.21.9/config.yml index 809bab9..de6f020 100644 --- a/defaultconfigs/1.21.9/config.yml +++ b/defaultconfigs/1.21.9/config.yml @@ -3,6 +3,14 @@ # 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 +metric_type: auto + # All anvil cost will be capped to limit_repair_value if enabled. # # In other words: diff --git a/defaultconfigs/1.21/config.yml b/defaultconfigs/1.21/config.yml index 88eff1a..ca4f248 100644 --- a/defaultconfigs/1.21/config.yml +++ b/defaultconfigs/1.21/config.yml @@ -3,6 +3,14 @@ # 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 +metric_type: auto + # All anvil cost will be capped to limit_repair_value if enabled. # # In other words: diff --git a/src/main/kotlin/io/delilaheve/CustomAnvil.kt b/src/main/kotlin/io/delilaheve/CustomAnvil.kt index 902b76d..e95622d 100644 --- a/src/main/kotlin/io/delilaheve/CustomAnvil.kt +++ b/src/main/kotlin/io/delilaheve/CustomAnvil.kt @@ -23,6 +23,7 @@ import xyz.alexcrea.cuanvil.update.ModrinthUpdateChecker import xyz.alexcrea.cuanvil.update.PluginSetDefault import xyz.alexcrea.cuanvil.update.UpdateHandler import xyz.alexcrea.cuanvil.util.Metrics +import xyz.alexcrea.cuanvil.util.MetricsUtil import java.io.File import java.io.FileReader import java.util.logging.Level @@ -34,7 +35,6 @@ open class CustomAnvil : JavaPlugin() { companion object { // pluginIDS - private const val bstatsPluginId = 20923 private const val modrinthPluginID = "S75Ueiq9" // Permission string required to use the plugin's features @@ -156,15 +156,17 @@ open class CustomAnvil : JavaPlugin() { } // Load metrics - try { - Metrics(this, bstatsPluginId) - } catch (_: Exception) {} + 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() diff --git a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt index afb009f..37ec7c0 100644 --- a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt +++ b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt @@ -21,6 +21,8 @@ object ConfigOptions { // Path for config values // ---------------------- + const val METRIC_TYPE = "metric_type" + 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" diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt index 2a46fa8..e1c2b34 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt @@ -20,7 +20,6 @@ class CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { "gui", editConfigCommand, "reload", ReloadExecutor(), "diagnostic", DiagnosticExecutor(), - //"debug", DebugExecutor(), ) init { diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DebugExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DebugExecutor.kt deleted file mode 100644 index e7cbe9e..0000000 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DebugExecutor.kt +++ /dev/null @@ -1,25 +0,0 @@ -package xyz.alexcrea.cuanvil.command - -import io.delilaheve.CustomAnvil -import org.bukkit.command.Command -import org.bukkit.command.CommandSender -import java.util.logging.Level - -class DebugExecutor : CASubCommand() { - - override fun executeCommand( - sender: CommandSender, - cmd: Command, - cmdstr: String, - args: Array - ): Boolean { - CustomAnvil.instance.logger.log(Level.SEVERE, "aaaaaaaaaaaaaaaaaaa"); - return true - } - - override fun tabCompleter(sender: CommandSender, args: Array, list: MutableList) { - //TODO - - } - -} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt index 9459444..6ddc8df 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt @@ -37,6 +37,21 @@ 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 result = when (packetManagerClass) { + PaperPacketManager::class.java -> "Paper NMS" + ProtocoLibWrapper::class.java -> "Protocolib" + NoPacketManager::class.java -> "None" + else -> "Version Specific" + + } + + return "$result ${if(packetManager.canSetInstantBuild) '✅' else '❌'}" + } } enum class DiagParams(val value: String) { @@ -124,7 +139,10 @@ class DiagnosticExecutor: CASubCommand() { fun diagnostic(sender: CommandSender, stb: StringBuilder, params: Set){ stb.append("Server Info\n") - stb.append("\nPlugin Version: ").append(CustomAnvil.instance.description.version) + 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") @@ -181,21 +199,6 @@ class DiagnosticExecutor: CASubCommand() { simulateAnvil(player, stb, sword, enchantedBook, enchantedSword) } - private fun fetchNMSType(): String { - val packetManager = DependencyManager.packetManager - val packetManagerClass = packetManager.javaClass - - val result = when (packetManagerClass) { - PaperPacketManager::class.java -> "Paper NMS" - ProtocoLibWrapper::class.java -> "Protocolib" - NoPacketManager::class.java -> "None" - else -> "Version Specific" - - } - - return "$result ${if(packetManager.canSetInstantBuild) '✅' else '❌'}" - } - private val Plugin.pluginNameDisplay: String get() { return this.name + " v" + this.description.version 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..3b912bf --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt @@ -0,0 +1,73 @@ +package xyz.alexcrea.cuanvil.util + +import dev.faststats.bukkit.BukkitMetrics +import dev.faststats.core.ErrorTracker +import dev.faststats.core.data.Metric +import io.delilaheve.CustomAnvil +import io.delilaheve.util.ConfigOptions +import xyz.alexcrea.cuanvil.command.DiagnosticExecutor +import xyz.alexcrea.cuanvil.config.ConfigHolder + +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: BukkitMetrics? = null + + fun loadMetrics(plugin: CustomAnvil) { + val metricString = ConfigHolder.DEFAULT_CONFIG.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) + //TODO nms type custom chart + } catch (_: Exception) {} + } + + if(metricType.allowFastStats) { + ERROR_TRACKER = ErrorTracker.contextAware(); + FAST_STATS_METRICS = BukkitMetrics.factory() + .addMetric(Metric.string("nms_type") { nmsType }) + .addMetric(Metric.bool("using_alpha") { isAlpha }) + .errorTracker(ERROR_TRACKER) + .token(FASTSTATS_TOKEN) + .create(plugin) + + FAST_STATS_METRICS!!.ready() + } + } + + fun shutdownMetrics() { + FAST_STATS_METRICS?.shutdown() + } + + fun trackError(e: Throwable) { + ERROR_TRACKER?.trackError(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/resources/config.yml b/src/main/resources/config.yml index c1fe214..88ed56b 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,8 +1,16 @@ # -# It is recommended that you use /configanvil to edit theses config. +# 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 ! # +# 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 +metric_type: auto + # All anvil cost will be capped to limit_repair_value if enabled. # # In other words: From 48f0cab15df74b20f7cd25d758038e279d77269d Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 3 Mar 2026 00:21:03 +0100 Subject: [PATCH 054/166] error logging and bstats simple pie --- build.gradle.kts | 4 ++-- .../alexcrea/cuanvil/config/ConfigHolder.java | 4 ++++ .../cuanvil/enchant/CAEnchantmentRegistry.java | 5 ++++- .../gui/config/ask/ConfirmActionGui.java | 2 ++ .../elements/EnchantConflictSubSettingGui.java | 3 +++ src/main/kotlin/io/delilaheve/CustomAnvil.kt | 18 +++++++++++++----- .../cuanvil/dependency/DependencyManager.kt | 5 +++++ .../plugins/DisenchantmentDependency.kt | 2 ++ .../xyz/alexcrea/cuanvil/util/MetricsUtil.kt | 3 ++- 9 files changed, 37 insertions(+), 9 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index e1448f1..903c988 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -165,8 +165,8 @@ tasks { archiveFileName.set(name) // Shadow necessary dependency - relocate("com.github.stefvanschie.inventoryframework", "xyz.alexcrea.customanvil.inventoryframework") - relocate("dev.faststats", "xyz.alexcrea.customanvil.faststats") + relocate("com.github.stefvanschie.inventoryframework", "xyz.alexcrea.cuanvil.inventoryframework") + relocate("dev.faststats", "xyz.alexcrea.cuanvil.faststats") filesMatching("plugin.yml") { expand( diff --git a/src/main/java/xyz/alexcrea/cuanvil/config/ConfigHolder.java b/src/main/java/xyz/alexcrea/cuanvil/config/ConfigHolder.java index 2037e23..f6a7e80 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/config/ConfigHolder.java +++ b/src/main/java/xyz/alexcrea/cuanvil/config/ConfigHolder.java @@ -9,6 +9,7 @@ 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; @@ -145,6 +146,7 @@ public abstract class ConfigHolder { 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 @@ -275,6 +277,7 @@ public abstract class ConfigHolder { 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); @@ -312,6 +315,7 @@ public abstract class ConfigHolder { 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; } diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentRegistry.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentRegistry.java index 4634a11..854ed55 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentRegistry.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentRegistry.java @@ -10,6 +10,7 @@ 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; @@ -85,11 +86,13 @@ public class CAEnchantmentRegistry { 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", - new IllegalStateException("enchantment " + enchantment.getKey() + " was already registered")); + error); + MetricsUtil.INSTANCE.trackError(error); return false; } 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 index 56bf848..5839663 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/ConfirmActionGui.java +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/ConfirmActionGui.java @@ -11,6 +11,7 @@ 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; @@ -41,6 +42,7 @@ public class ConfirmActionGui extends AbstractAskGui { success = onConfirm.get(); } catch (Exception e) { CustomAnvil.instance.getLogger().log(Level.WARNING, "Could not process confirmation supplier.", e); + MetricsUtil.INSTANCE.trackError(e); success = false; } 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 index 99bba84..97bdfcb 100644 --- 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 @@ -25,6 +25,7 @@ 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; @@ -264,6 +265,7 @@ public class EnchantConflictSubSettingGui extends MappedToListSubSettingGui impl 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 @@ -308,6 +310,7 @@ public class EnchantConflictSubSettingGui extends MappedToListSubSettingGui impl 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 diff --git a/src/main/kotlin/io/delilaheve/CustomAnvil.kt b/src/main/kotlin/io/delilaheve/CustomAnvil.kt index e95622d..9b2b696 100644 --- a/src/main/kotlin/io/delilaheve/CustomAnvil.kt +++ b/src/main/kotlin/io/delilaheve/CustomAnvil.kt @@ -22,7 +22,6 @@ 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.Metrics import xyz.alexcrea.cuanvil.util.MetricsUtil import java.io.File import java.io.FileReader @@ -118,7 +117,8 @@ open class CustomAnvil : JavaPlugin() { try { legacyCheck() } catch (e: Exception) { - logger.log(Level.SEVERE, "error trying to check for legacy system" , e) + logger.log(Level.SEVERE, "error trying to check for legacy system", e) + MetricsUtil.trackError(e) if(trySafeStart()) return } @@ -126,7 +126,8 @@ open class CustomAnvil : JavaPlugin() { try { prepareCommand() } catch (e: Exception) { - logger.log(Level.SEVERE, "error trying to register commands" , e) + logger.log(Level.SEVERE, "error trying to register commands", e) + MetricsUtil.trackError(e) if(trySafeStart()) return } @@ -136,6 +137,7 @@ open class CustomAnvil : JavaPlugin() { 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 } @@ -144,6 +146,7 @@ open class CustomAnvil : JavaPlugin() { DependencyManager.loadDependency() } catch (e: Exception) { logger.log(Level.SEVERE, "error loading dependency compatibility", e) + MetricsUtil.trackError(e) if(tryDirtyStart()) return } @@ -152,6 +155,7 @@ open class CustomAnvil : JavaPlugin() { registerListeners() } catch (e: Exception) { logger.log(Level.SEVERE, "error registering listeners", e) + MetricsUtil.trackError(e) if(tryDirtyStart()) return } @@ -171,7 +175,8 @@ open class CustomAnvil : JavaPlugin() { try { loadEnchantmentSystem() } catch (e: Exception) { - logger.log(Level.SEVERE, "error initializing enchantment ssytem", e) + logger.log(Level.SEVERE, "error initializing enchantment system", e) + MetricsUtil.trackError(e) tryDirtyStart() } } @@ -198,7 +203,10 @@ open class CustomAnvil : JavaPlugin() { ModrinthUpdateChecker(modrinthPluginID, loader, null) .setFeatured(featured) - .setOnError { logger.log(Level.WARNING, "error trying to fetch latest update", it) } + .setOnError { + logger.log(Level.WARNING, "error trying to fetch latest update", it) + MetricsUtil.trackError(it) + } .checkVersion { latestVer: String? -> CustomAnvil.latestVer = latestVer if(latestVer == null || version.contains(latestVer)) return@checkVersion diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt index 42383b6..6794af0 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt @@ -27,6 +27,7 @@ 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.AnvilUseType +import xyz.alexcrea.cuanvil.util.MetricsUtil.trackError import java.util.logging.Level object DependencyManager { @@ -141,6 +142,7 @@ object DependencyManager { "Error while trying to handle custom anvil supported plugin: ", e ) + trackError(e) // Just in case to avoid illegal items event.inventory.setItem(ANVIL_OUTPUT_SLOT, null) @@ -180,6 +182,7 @@ object DependencyManager { "Error while trying to handle custom anvil supported plugin: ", e ) + trackError(e) // Just in case to avoid illegal items event.inventory.setItem(ANVIL_OUTPUT_SLOT, null) @@ -233,6 +236,7 @@ object DependencyManager { "Error while trying to handle custom anvil supported plugin: ", e ) + trackError(e) // Just in case to avoid illegal items event.inventory.setItem(ANVIL_OUTPUT_SLOT, null) @@ -262,6 +266,7 @@ object DependencyManager { "Error while trying to handle custom anvil supported plugin: ", e ) + trackError(e) // Just in case to avoid illegal items event.inventory.setItem(ANVIL_OUTPUT_SLOT, null) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt index f014c8a..089a5fb 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt @@ -16,6 +16,7 @@ import org.bukkit.inventory.AnvilInventory import org.bukkit.inventory.ItemStack import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener import xyz.alexcrea.cuanvil.util.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.MetricsUtil.trackError import java.util.logging.Level import kotlin.reflect.KClass @@ -38,6 +39,7 @@ class DisenchantmentDependency { Level.SEVERE, "Could not initialize disenchantment support" + "please report this bug to the developer", e ) + trackError(e) } } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt index 3b912bf..460ee3f 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt @@ -25,7 +25,8 @@ object MetricsUtil { if(metricType.allowBStats) { try { val metric = Metrics(plugin, BSTATS_PLUGIN_ID) - //TODO nms type custom chart + metric.addCustomChart(Metrics.SimplePie("nms_type") { nmsType }) + metric.addCustomChart(Metrics.SimplePie("using_alpha") { isAlpha.toString() }) } catch (_: Exception) {} } From 7044860267d9934a5e97f9cdce9940defb369193 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 3 Mar 2026 03:12:35 +0100 Subject: [PATCH 055/166] some fix and add disable error telemetry config --- defaultconfigs/1.18/config.yml | 7 ++++++- defaultconfigs/1.21.11/config.yml | 7 ++++++- defaultconfigs/1.21.9/config.yml | 7 ++++++- defaultconfigs/1.21/config.yml | 7 ++++++- .../io/delilaheve/util/ConfigOptions.kt | 1 + .../cuanvil/command/CustomAnvilCmd.kt | 20 ++++++++++++++----- .../xyz/alexcrea/cuanvil/util/MetricsUtil.kt | 11 +++++++--- src/main/resources/config.yml | 7 ++++++- 8 files changed, 54 insertions(+), 13 deletions(-) diff --git a/defaultconfigs/1.18/config.yml b/defaultconfigs/1.18/config.yml index 69a0e18..47070ba 100644 --- a/defaultconfigs/1.18/config.yml +++ b/defaultconfigs/1.18/config.yml @@ -8,9 +8,14 @@ # 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 +# 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: diff --git a/defaultconfigs/1.21.11/config.yml b/defaultconfigs/1.21.11/config.yml index c281de8..0d9e089 100644 --- a/defaultconfigs/1.21.11/config.yml +++ b/defaultconfigs/1.21.11/config.yml @@ -8,9 +8,14 @@ # 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 +# 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: diff --git a/defaultconfigs/1.21.9/config.yml b/defaultconfigs/1.21.9/config.yml index de6f020..d3538b2 100644 --- a/defaultconfigs/1.21.9/config.yml +++ b/defaultconfigs/1.21.9/config.yml @@ -8,9 +8,14 @@ # 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 +# 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: diff --git a/defaultconfigs/1.21/config.yml b/defaultconfigs/1.21/config.yml index ca4f248..c1677fb 100644 --- a/defaultconfigs/1.21/config.yml +++ b/defaultconfigs/1.21/config.yml @@ -8,9 +8,14 @@ # 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 +# 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: diff --git a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt index 37ec7c0..8bd76d2 100644 --- a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt +++ b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt @@ -22,6 +22,7 @@ object ConfigOptions { // ---------------------- 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" diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt index e1c2b34..f1fcb88 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt @@ -6,6 +6,7 @@ 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 CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { @@ -37,10 +38,14 @@ class CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { args: Array ): Boolean { // Find sub command to execute based on the provided command name - val subcmd: CASubCommand? = if(args.isEmpty()) { - editConfigCommand + val subcmd: CASubCommand? + val newargs: Array + if(args.isEmpty()) { + subcmd = editConfigCommand + newargs = args }else { - commands[args[0].lowercase()] + subcmd = commands[args[0].lowercase()] + newargs = args.copyOfRange(1, args.size) } if(subcmd == null) { @@ -48,8 +53,13 @@ class CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { return true } - val newargs = args.copyOfRange(1, args.size) - return subcmd.executeCommand(sender, cmd, cmdstr, newargs) + 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( diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt index 460ee3f..ad441a5 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt @@ -17,7 +17,8 @@ object MetricsUtil { private var FAST_STATS_METRICS: BukkitMetrics? = null fun loadMetrics(plugin: CustomAnvil) { - val metricString = ConfigHolder.DEFAULT_CONFIG.config.getString(ConfigOptions.METRIC_TYPE, MetricType.AUTO.value)!! + 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() @@ -31,15 +32,19 @@ object MetricsUtil { } if(metricType.allowFastStats) { - ERROR_TRACKER = ErrorTracker.contextAware(); + val reportErrors = config.getBoolean(ConfigOptions.METRIC_COLLECT_ERROR, true) + if(reportErrors) + ERROR_TRACKER = ErrorTracker.contextAware() + FAST_STATS_METRICS = BukkitMetrics.factory() .addMetric(Metric.string("nms_type") { nmsType }) + .addMetric(Metric.bool("replace_too_expensive") { ConfigOptions.doReplaceTooExpensive }) .addMetric(Metric.bool("using_alpha") { isAlpha }) .errorTracker(ERROR_TRACKER) .token(FASTSTATS_TOKEN) .create(plugin) - FAST_STATS_METRICS!!.ready() + if(reportErrors) FAST_STATS_METRICS!!.ready() } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 88ed56b..7cc1768 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -8,9 +8,14 @@ # 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 +# 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: From 621222fc01002883f501ea1050896128e87d8b9c Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 3 Mar 2026 03:35:15 +0100 Subject: [PATCH 056/166] better telemetry disclosure --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e1d7ec6..fe552b8 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Please note that 1.16.5 to 1.17.1 are not officially supported. Run at your own ### 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)​ +(Please note that the wiki is currently incomplete) --- @@ -76,7 +76,14 @@ For information about the API, please refer to [the Wiki](https://github.com/ale see [Here](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs) --- -Custom anvil [use bstat](https://bstats.org/plugin/bukkit/Unsafe%20Enchants%20Plus/20923) for metric. You can [disable it](https://bstats.org/getting-started) if you like. +### 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) From bc7ed5af85b220b3f887ead5694c54b87667bc49 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 3 Mar 2026 03:59:58 +0100 Subject: [PATCH 057/166] add permission on tab completer and pre execute --- .../kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt index f1fcb88..c3de43b 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt @@ -24,8 +24,6 @@ class CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { ) init { - - println(plugin.getCommand(genericCommandName)) val self = plugin.getCommand(genericCommandName)!! self.setExecutor(this) self.tabCompleter = this @@ -48,7 +46,7 @@ class CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { newargs = args.copyOfRange(1, args.size) } - if(subcmd == null) { + if(subcmd == null || !subcmd.allowed(sender)) { sender.sendMessage("Invalid subcommand. run `$cmdstr help` to see available commands") return true } @@ -78,6 +76,8 @@ class CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { if(subcmd != null) { val newArgs = args.copyOfRange(1, args.size) + if(!subcmd.allowed(sender)) return result + subcmd.tabCompleter(sender, newArgs, result) } } From 04408350139044b5b01765c742e3c43696664933 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 3 Mar 2026 04:08:04 +0100 Subject: [PATCH 058/166] Help Command --- src/main/kotlin/io/delilaheve/CustomAnvil.kt | 4 +-- .../alexcrea/cuanvil/command/CASubCommand.kt | 4 +++ ...ustomAnvilCmd.kt => CustomAnvilCommand.kt} | 11 +++++-- .../cuanvil/command/DiagnosticExecutor.kt | 4 +++ .../cuanvil/command/EditConfigExecutor.kt | 4 +++ .../alexcrea/cuanvil/command/HelpExecutor.kt | 32 +++++++++++++++++++ .../cuanvil/command/ReloadExecutor.kt | 4 +++ 7 files changed, 58 insertions(+), 5 deletions(-) rename src/main/kotlin/xyz/alexcrea/cuanvil/command/{CustomAnvilCmd.kt => CustomAnvilCommand.kt} (88%) create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/command/HelpExecutor.kt diff --git a/src/main/kotlin/io/delilaheve/CustomAnvil.kt b/src/main/kotlin/io/delilaheve/CustomAnvil.kt index 9b2b696..44ca0a8 100644 --- a/src/main/kotlin/io/delilaheve/CustomAnvil.kt +++ b/src/main/kotlin/io/delilaheve/CustomAnvil.kt @@ -6,7 +6,7 @@ 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.CustomAnvilCmd +import xyz.alexcrea.cuanvil.command.CustomAnvilCommand import xyz.alexcrea.cuanvil.command.EditConfigExecutor import xyz.alexcrea.cuanvil.command.ReloadExecutor import xyz.alexcrea.cuanvil.config.ConfigHolder @@ -309,7 +309,7 @@ open class CustomAnvil : JavaPlugin() { command = getCommand(commandConfigName) command?.setExecutor(EditConfigExecutor()) - CustomAnvilCmd(this) + CustomAnvilCommand(this) } } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt index 85c1a58..b06d07f 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt @@ -39,4 +39,8 @@ abstract class CASubCommand: CommandExecutor { 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/CustomAnvilCmd.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCommand.kt similarity index 88% rename from src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt rename to src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCommand.kt index c3de43b..e5e689e 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCmd.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CustomAnvilCommand.kt @@ -9,7 +9,7 @@ import org.bukkit.command.TabCompleter import xyz.alexcrea.cuanvil.util.MetricsUtil import java.util.ArrayList -class CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { +class CustomAnvilCommand(plugin: CustomAnvil) : CommandExecutor, TabCompleter { // Name of the generic command companion object { @@ -17,16 +17,20 @@ class CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { } 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( @@ -68,8 +72,9 @@ class CustomAnvilCmd(plugin: CustomAnvil) : CommandExecutor, TabCompleter { ): MutableList { val result = ArrayList() if(args.size < 2) { - for (cmd in commands) { - result.add(cmd.key) + for ((key, cmd) in commands) { + if(!cmd.allowed(sender)) continue + result.add(key) } } else { val subcmd = commands[args[0].lowercase()] diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt index 6ddc8df..e550bd1 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt @@ -204,6 +204,10 @@ class DiagnosticExecutor: CASubCommand() { 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() diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/EditConfigExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/EditConfigExecutor.kt index d489db2..fba89b7 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/EditConfigExecutor.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/EditConfigExecutor.kt @@ -38,4 +38,8 @@ class EditConfigExecutor: CASubCommand() { 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 index f3f97a7..ef23e7d 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/ReloadExecutor.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/ReloadExecutor.kt @@ -39,6 +39,10 @@ class ReloadExecutor : CASubCommand() { 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 */ From 77c8494166ae426ef7b116109fd7c80204f28812 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 3 Mar 2026 04:23:07 +0100 Subject: [PATCH 059/166] Add last detected error in diag optional --- .../cuanvil/command/DiagnosticExecutor.kt | 17 +++++++++++++++++ .../xyz/alexcrea/cuanvil/util/MetricsUtil.kt | 3 +++ 2 files changed, 20 insertions(+) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt index e550bd1..a89318c 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt @@ -29,6 +29,7 @@ import xyz.alexcrea.cuanvil.dependency.packet.ProtocoLibWrapper import xyz.alexcrea.cuanvil.dependency.packet.versions.PaperPacketManager 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 @@ -59,6 +60,7 @@ class DiagnosticExecutor: CASubCommand() { 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 { @@ -154,6 +156,8 @@ class DiagnosticExecutor: CASubCommand() { .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) } @@ -168,6 +172,10 @@ class DiagnosticExecutor: CASubCommand() { 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) { @@ -323,5 +331,14 @@ class DiagnosticExecutor: CASubCommand() { }") } + + 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/util/MetricsUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt index ad441a5..07e21fe 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt @@ -52,8 +52,11 @@ object MetricsUtil { FAST_STATS_METRICS?.shutdown() } + var lastError: Throwable? = null + fun trackError(e: Throwable) { ERROR_TRACKER?.trackError(e) + lastError = e } fun trackError(message: String) { From 5caa56be592ac1066fd249c7015a08a47f670799 Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:07:38 +0100 Subject: [PATCH 060/166] lowercase .md --- CREDITS.MD => CREDITS.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CREDITS.MD => CREDITS.md (100%) diff --git a/CREDITS.MD b/CREDITS.md similarity index 100% rename from CREDITS.MD rename to CREDITS.md From 7bb0c1523d78434e9690d453f719927c8bb1be57 Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:07:54 +0100 Subject: [PATCH 061/166] lowercase .md --- COMPATIBILITY.MD => COMPATIBILITY.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename COMPATIBILITY.MD => COMPATIBILITY.md (100%) diff --git a/COMPATIBILITY.MD b/COMPATIBILITY.md similarity index 100% rename from COMPATIBILITY.MD rename to COMPATIBILITY.md From 9b8a2d0f329b0463453fe26f26b505ba728cd5e3 Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:21:40 +0100 Subject: [PATCH 062/166] [ci skip] update permissions in readme --- README.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index fe552b8..22d9da1 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,20 @@ the plugin can be downloaded on --- ### Permissions: ```yml +# 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) -ca.command.reload: Allow administrator to reload the plugin's configs -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 +# 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.hex: Allow player to use hexadecimal color on rename if enabled (toggleable) @@ -47,10 +53,9 @@ ca.lore_edit.paper: Allow player to edit lore via paper if enabled (toggleable) ``` ### Commands -```yml -anvilconfigreload or carl: Reload every config of this plugin -customanvilconfig or configanvil: open a menu for administrator to edit plugin's config in game -``` + +run `customanvil help` to get information about available commands (need permissions to use them) + ### Supported Plugins See the [Compatibility list](https://github.com/alexcrea/CustomAnvil/blob/v1.x.x/COMPATIBILITY.md) From e08a02a84adbecbbb308716ea9f85684bf813cb9 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 3 Mar 2026 15:46:35 +0100 Subject: [PATCH 063/166] fix class error on java 17 --- .../cuanvil/command/DiagnosticExecutor.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt index a89318c..5d71119 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt @@ -43,14 +43,18 @@ class DiagnosticExecutor: CASubCommand() { val packetManager = DependencyManager.packetManager val packetManagerClass = packetManager.javaClass - val result = when (packetManagerClass) { - PaperPacketManager::class.java -> "Paper NMS" - ProtocoLibWrapper::class.java -> "Protocolib" - NoPacketManager::class.java -> "None" - else -> "Version Specific" - + 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 '❌'}" } } From b532ce7dc6170b17119d8ae35ab7277f09df9047 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 3 Mar 2026 15:51:41 +0100 Subject: [PATCH 064/166] avoid error on java < 21 --- .../xyz/alexcrea/cuanvil/util/MetricsUtil.kt | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt index 07e21fe..dbec6e2 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt @@ -32,22 +32,32 @@ object MetricsUtil { } if(metricType.allowFastStats) { - val reportErrors = config.getBoolean(ConfigOptions.METRIC_COLLECT_ERROR, true) - if(reportErrors) - ERROR_TRACKER = ErrorTracker.contextAware() - - FAST_STATS_METRICS = BukkitMetrics.factory() - .addMetric(Metric.string("nms_type") { nmsType }) - .addMetric(Metric.bool("replace_too_expensive") { ConfigOptions.doReplaceTooExpensive }) - .addMetric(Metric.bool("using_alpha") { isAlpha }) - .errorTracker(ERROR_TRACKER) - .token(FASTSTATS_TOKEN) - .create(plugin) - - if(reportErrors) FAST_STATS_METRICS!!.ready() + // Check support java 21 (metric only work in java 21) + val versionParts = System.getProperty("java.version").split(".") + val majorVersion = versionParts[0].toInt() + if (majorVersion >= 21) 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 = BukkitMetrics.factory() + .addMetric(Metric.string("nms_type") { nmsType }) + .addMetric(Metric.bool("replace_too_expensive") { ConfigOptions.doReplaceTooExpensive }) + .addMetric(Metric.bool("using_alpha") { isAlpha }) + .errorTracker(ERROR_TRACKER) + .token(FASTSTATS_TOKEN) + .create(plugin) + + if(reportErrors) FAST_STATS_METRICS!!.ready() + } + fun shutdownMetrics() { FAST_STATS_METRICS?.shutdown() } From ab3e4a32baa1e5955d8c48620e4f5091fdc9ac31 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 3 Mar 2026 16:19:45 +0100 Subject: [PATCH 065/166] [ci skip] version bump --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 903c988..457fd2e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } group = "xyz.alexcrea" -version = "1.16.0" +version = "1.16.1" val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null val isPreRelease = System.getenv("IS_GITHUB_PRERELEASE") == "true" From 6a4c861eab2765cb3157791c7bdd2f13f5badc72 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 9 Mar 2026 21:22:49 +0100 Subject: [PATCH 066/166] update faststat --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 457fd2e..2a7e922 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } group = "xyz.alexcrea" -version = "1.16.1" +version = "1.16.2" val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null val isPreRelease = System.getenv("IS_GITHUB_PRERELEASE") == "true" @@ -52,7 +52,7 @@ dependencies { compileOnly("org.spigotmc:spigot-api:1.18-R0.1-SNAPSHOT") // fast stats - implementation("dev.faststats.metrics:bukkit:0.16.0") + implementation("dev.faststats.metrics:bukkit:0.18.0") // minimessage implementation("net.kyori:adventure-text-minimessage:4.25.0") From 6afe51acca892eb63dca15a0a8ca0d6326abd8c1 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 9 Mar 2026 22:16:35 +0100 Subject: [PATCH 067/166] reduce faststat java major version --- src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt index dbec6e2..80a84c2 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt @@ -32,10 +32,10 @@ object MetricsUtil { } if(metricType.allowFastStats) { - // Check support java 21 (metric only work in java 21) + // Check support java 17 (metric only work in java 17) val versionParts = System.getProperty("java.version").split(".") val majorVersion = versionParts[0].toInt() - if (majorVersion >= 21) try { + if (majorVersion >= 17) try { faststatTelemetry(plugin, nmsType, isAlpha) } catch (_: Throwable) {} } From f59071f504bf492450ef56341934137a5acefcbc Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 9 Mar 2026 22:43:57 +0100 Subject: [PATCH 068/166] prioritize paper nms on paper servers --- .../cuanvil/command/DiagnosticExecutor.kt | 1 - .../dependency/packet/PacketManagerSelector.kt | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt index 5d71119..053a3fc 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/DiagnosticExecutor.kt @@ -26,7 +26,6 @@ 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.dependency.packet.versions.PaperPacketManager import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener import xyz.alexcrea.cuanvil.util.MetricsUtil diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerSelector.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerSelector.kt index f38b9e4..775d197 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerSelector.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerSelector.kt @@ -8,14 +8,22 @@ import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil 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 return if (forceProtocolib) protocolibIfPresent - else - reobfPacketManager ?: - if(PlatformUtil.isPaper) PaperPacketManager() - else protocolibIfPresent + else { + try { + Class.forName(PAPER_CRAFT_PLAYER_CLASS) + + return PaperPacketManager() + } catch (_: ClassNotFoundException) { + return reobfPacketManager ?: protocolibIfPresent + } + } } private val protocolibIfPresent: PacketManager From c96dd7d308324dedae17db63a78da657385ad305 Mon Sep 17 00:00:00 2001 From: alexd <42614139+alexcrea@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:57:47 +0100 Subject: [PATCH 069/166] simplify and extend enchant limit --- defaultconfigs/1.18/config.yml | 8 +-- defaultconfigs/1.21.11/config.yml | 8 +-- defaultconfigs/1.21.9/config.yml | 8 +-- defaultconfigs/1.21/config.yml | 8 +-- .../alexcrea/cuanvil/api/EnchantmentApi.java | 5 +- .../config/global/EnchantLimitConfigGui.java | 16 +++-- .../gui/config/settings/IntSettingsGui.java | 11 ++- .../cuanvil/update/PluginSetDefault.java | 1 - .../io/delilaheve/util/ConfigOptions.kt | 71 +++++++------------ src/main/resources/config.yml | 8 +-- 10 files changed, 58 insertions(+), 86 deletions(-) diff --git a/defaultconfigs/1.18/config.yml b/defaultconfigs/1.18/config.yml index 47070ba..5a85d1d 100644 --- a/defaultconfigs/1.18/config.yml +++ b/defaultconfigs/1.18/config.yml @@ -87,18 +87,14 @@ permission_needed_for_color: true # Valid values include 0 to 1000. use_of_color_cost: 0 -# Default limit to apply to any enchants missing from enchant_limits -# -# Valid values include 1 to 1000 -default_limit: 5 - # 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 1 - 255 for each enchantment +# Valid range of 0 - 255 for each enchantment +# -1 mean keep default enchant_limits: minecraft:aqua_affinity: 1 minecraft:binding_curse: 1 diff --git a/defaultconfigs/1.21.11/config.yml b/defaultconfigs/1.21.11/config.yml index 0d9e089..267df3d 100644 --- a/defaultconfigs/1.21.11/config.yml +++ b/defaultconfigs/1.21.11/config.yml @@ -89,18 +89,14 @@ permission_needed_for_color: true # Valid values include 0 to 1000. use_of_color_cost: 0 -# Default limit to apply to any enchants missing from enchant_limits -# -# Valid values include 1 to 1000 -default_limit: 5 - # 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 1 - 255 for each enchantment +# Valid range of 0 - 255 for each enchantment +# -1 mean keep default enchant_limits: minecraft:aqua_affinity: 1 minecraft:binding_curse: 1 diff --git a/defaultconfigs/1.21.9/config.yml b/defaultconfigs/1.21.9/config.yml index d3538b2..87d03a9 100644 --- a/defaultconfigs/1.21.9/config.yml +++ b/defaultconfigs/1.21.9/config.yml @@ -87,18 +87,14 @@ permission_needed_for_color: true # Valid values include 0 to 1000. use_of_color_cost: 0 -# Default limit to apply to any enchants missing from enchant_limits -# -# Valid values include 1 to 1000 -default_limit: 5 - # 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 1 - 255 for each enchantment +# Valid range of 0 - 255 for each enchantment +# -1 mean keep default enchant_limits: minecraft:aqua_affinity: 1 minecraft:binding_curse: 1 diff --git a/defaultconfigs/1.21/config.yml b/defaultconfigs/1.21/config.yml index c1677fb..b6b620c 100644 --- a/defaultconfigs/1.21/config.yml +++ b/defaultconfigs/1.21/config.yml @@ -87,18 +87,14 @@ permission_needed_for_color: true # Valid values include 0 to 1000. use_of_color_cost: 0 -# Default limit to apply to any enchants missing from enchant_limits -# -# Valid values include 1 to 1000 -default_limit: 5 - # 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 1 - 255 for each enchantment +# Valid range of 0 - 255 for each enchantment +# -1 mean keep default enchant_limits: minecraft:aqua_affinity: 1 minecraft:binding_curse: 1 diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/EnchantmentApi.java b/src/main/java/xyz/alexcrea/cuanvil/api/EnchantmentApi.java index 76f1ac2..ac98225 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/api/EnchantmentApi.java +++ b/src/main/java/xyz/alexcrea/cuanvil/api/EnchantmentApi.java @@ -1,6 +1,7 @@ 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; @@ -180,13 +181,13 @@ public class EnchantmentApi { private static boolean tryWriteDefaultConfig(FileConfiguration defaultConfig, CAEnchantment enchantment, boolean override) { boolean hasChange = false; - String levelPath = "enchant_limits." + enchantment.getKey(); + String levelPath = ConfigOptions.ENCHANT_LIMIT_ROOT + "." + enchantment.getKey(); if(override || !defaultConfig.isSet(levelPath)){ defaultConfig.set(levelPath, enchantment.defaultMaxLevel()); hasChange = true; } - String basePath = "enchant_values." + enchantment.getKey(); + String basePath = ConfigOptions.ENCHANT_VALUES_ROOT + "." + enchantment.getKey(); EnchantmentRarity rarity = enchantment.defaultRarity(); String itemPath = basePath + ".item"; 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 index d624bff..a95a5bb 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantLimitConfigGui.java +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantLimitConfigGui.java @@ -17,7 +17,7 @@ import java.util.Locale; */ public class EnchantLimitConfigGui extends AbstractEnchantConfigGui { - private static final String SECTION_NAME = "enchant_limits"; + private static final String SECTION_NAME = ConfigOptions.ENCHANT_LIMIT_ROOT; private static EnchantLimitConfigGui INSTANCE = null; @@ -41,18 +41,26 @@ public class EnchantLimitConfigGui extends AbstractEnchantConfigGui §e" + planned + " §r(§c-" + (now - planned) + "§r)"); + meta.setDisplayName("§e" + displayNow + " §f-> §e" + planned + " §r(§c-" + holder.valueDisplayName(now - planned) + "§r)"); meta.setLore(Collections.singletonList(AbstractSettingGui.CLICK_LORE)); item.setItemMeta(meta); @@ -116,7 +117,7 @@ public class IntSettingsGui extends AbstractSettingGui { ItemMeta meta = item.getItemMeta(); assert meta != null; - meta.setDisplayName("§e" + now + " §f-> §e" + planned + " §r(§a+" + (planned - now) + "§r)"); + meta.setDisplayName("§e" + displayNow + " §f-> §e" + planned + " §r(§a+" + holder.valueDisplayName(planned - now) + "§r)"); meta.setLore(Collections.singletonList(AbstractSettingGui.CLICK_LORE)); item.setItemMeta(meta); @@ -131,7 +132,7 @@ public class IntSettingsGui extends AbstractSettingGui { ItemMeta resultMeta = resultPaper.getItemMeta(); assert resultMeta != null; - resultMeta.setDisplayName("§fValue: §e" + now); + resultMeta.setDisplayName("§fValue: §e" + displayNow); resultMeta.setLore(holder.displayLore); resultPaper.setItemMeta(resultMeta); @@ -389,6 +390,10 @@ public class IntSettingsGui extends AbstractSettingGui { return getItem(itemMat, CasedStringUtil.detectToUpperSpacedCase(configPath)); } + public String valueDisplayName(int value) { + return String.valueOf(value); + } + } } diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/PluginSetDefault.java b/src/main/java/xyz/alexcrea/cuanvil/update/PluginSetDefault.java index f41842d..774c5da 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/update/PluginSetDefault.java +++ b/src/main/java/xyz/alexcrea/cuanvil/update/PluginSetDefault.java @@ -30,7 +30,6 @@ public class PluginSetDefault { 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, DEFAULT_LIMIT_PATH, DEFAULT_ENCHANT_LIMIT); // Lore Edit defaults for (@NotNull LoreEditType value : LoreEditType.values()) { diff --git a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt index 8bd76d2..c69f2fb 100644 --- a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt +++ b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt @@ -57,8 +57,6 @@ object ConfigOptions { const val ENCHANT_COUNT_LIMIT_DEFAULT = "$ENCHANT_COUNT_LIMIT_ROOT.default" const val ENCHANT_COUNT_LIMIT_ITEMS = "$ENCHANT_COUNT_LIMIT_ROOT.items" - const val DEFAULT_LIMIT_PATH = "default_limit" - const val ENCHANT_LIMIT_ROOT = "enchant_limits" const val ENCHANT_VALUES_ROOT = "enchant_values" @@ -102,8 +100,6 @@ object ConfigOptions { const val DEFAULT_PERMISSION_NEEDED_FOR_COLOR = true const val DEFAULT_USE_OF_COLOR_COST = 0 - const val DEFAULT_ENCHANT_LIMIT = 5 - // Debug flag private const val DEFAULT_DEBUG_LOG = false private const val DEFAULT_VERBOSE_DEBUG_LOG = false @@ -133,8 +129,7 @@ object ConfigOptions { val USE_OF_COLOR_COST_RANGE = 0..1000 // Valid range for an enchantment limit - @JvmField - val ENCHANT_LIMIT_RANGE = 1..255 + const val ENCHANT_LIMIT = 255 // Valid range for an enchantment count limit @JvmField @@ -348,16 +343,6 @@ object ConfigOptions { return WorkPenaltyPart(penaltyIncrease, penaltyAdditive, exclusivePenaltyIncrease, exclusivePenaltyAdditive) } - /** - * Default enchantment limit - */ - private val defaultEnchantLimit: Int - get() { - return ConfigHolder.DEFAULT_CONFIG - .config - .getInt(DEFAULT_LIMIT_PATH, DEFAULT_ENCHANT_LIMIT) - } - /** * Get material enchantment count limit * @@ -422,42 +407,37 @@ object ConfigOptions { * Get the given [enchantment]'s limit */ 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 != null) return limit + if (limit >= 0) return limit // Test legacy (name only) limit = enchantLimit(enchantment.enchantmentName) - if (limit != null) return limit + if (limit >= 0) return limit - // get default (and test old legacy if present) - return getDefaultLevel(enchantment.enchantmentName) + // Default to negative + return -1 } /** * Get the given [enchantmentName]'s limit */ - private fun enchantLimit(enchantmentName: String): Int? { + private fun enchantLimit(enchantmentName: String): Int { val path = "${ENCHANT_LIMIT_ROOT}.$enchantmentName" - return CustomAnvil.instance - .config - .getInt(path, ENCHANT_LIMIT_RANGE.first - 1) - .takeIf { it in ENCHANT_LIMIT_RANGE } - } - - /** - * Get default value if enchantment do not exist on config - */ - private fun getDefaultLevel( - enchantmentName: String, // compatibility with 1.20.5. TODO better update system - ): Int { - if (enchantmentName == "sweeping_edge") { - val limit = enchantLimit("sweeping") - if (limit != null) return limit - - } - return defaultEnchantLimit + return CustomAnvil.instance.config + .getInt(path, -1) } /** @@ -529,20 +509,20 @@ object ConfigOptions { fun maxBeforeMergeDisabled(enchantment: CAEnchantment): Int { val key = enchantment.key.toString() var value = maxBeforeMergeDisabled(key) - if (value != null) return value + if (value >= 0) return value // Legacy name val legacy = enchantment.enchantmentName value = maxBeforeMergeDisabled(legacy) - if (value != null) return value + if (value >= 0) return value if (key == "minecraft:sweeping_edge") { value = maxBeforeMergeDisabled("minecraft:sweeping") - if (value != null) return value + if (value >= 0) return value // legacy name of legacy enchantment name value = maxBeforeMergeDisabled("sweeping") - if (value != null) return value + if (value >= 0) return value } return DEFAULT_MAX_BEFORE_MERGE_DISABLED @@ -552,14 +532,13 @@ object ConfigOptions { * Get the given [enchantmentName]'s level before merge is disabled * a negative value would mean never disabled */ - private fun maxBeforeMergeDisabled(enchantmentName: String): Int? { + private fun maxBeforeMergeDisabled(enchantmentName: String): Int { // find if set val path = "${DISABLE_MERGE_OVER_ROOT}.$enchantmentName" return CustomAnvil.instance .config - .getInt(path, ENCHANT_LIMIT_RANGE.min() - 1) - .takeIf { it in ENCHANT_LIMIT_RANGE } + .getInt(path, -1) } fun isImmutable(key: NamespacedKey): Boolean { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 7cc1768..95729b7 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -89,18 +89,14 @@ permission_needed_for_color: true # Valid values include 0 to 1000. use_of_color_cost: 0 -# Default limit to apply to any enchants missing from enchant_limits -# -# Valid values include 1 to 1000 -default_limit: 5 - # 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 1 - 255 for each enchantment +# Valid range of 0 - 255 for each enchantment +# -1 mean keep default enchant_limits: minecraft:aqua_affinity: 1 minecraft:binding_curse: 1 From 882e50e44961478dafa6c3db9bb6651f6a2494e1 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 23 Mar 2026 18:04:14 +0100 Subject: [PATCH 070/166] remove force protocolib in config --- defaultconfigs/1.18/config.yml | 6 ------ defaultconfigs/1.21.11/config.yml | 6 ------ defaultconfigs/1.21.9/config.yml | 6 ------ defaultconfigs/1.21/config.yml | 6 ------ src/main/resources/config.yml | 6 ------ 5 files changed, 30 deletions(-) diff --git a/defaultconfigs/1.18/config.yml b/defaultconfigs/1.18/config.yml index 5a85d1d..eb45a8a 100644 --- a/defaultconfigs/1.18/config.yml +++ b/defaultconfigs/1.18/config.yml @@ -406,10 +406,4 @@ debug_log: false # Whether to show verbose debug logging debug_log_verbose: false -# In case something when wrong with CustomAnvil packet manager. -# If you see "missing class exception" or similar you may test this. -# If enabled and Protocolib absent or disabled "Replace to expensive" will not work. -# ProtocoLib may also be used if the server is in an "unsupported" version even if this option is disabled. -force_protocolib: false - configVersion: 1.11.0 diff --git a/defaultconfigs/1.21.11/config.yml b/defaultconfigs/1.21.11/config.yml index 267df3d..c4500a8 100644 --- a/defaultconfigs/1.21.11/config.yml +++ b/defaultconfigs/1.21.11/config.yml @@ -426,11 +426,5 @@ debug_log: false # Whether to show verbose debug logging debug_log_verbose: false -# In case something when wrong with CustomAnvil packet manager. -# If you see "missing class exception" or similar you may test this. -# If enabled and Protocolib absent or disabled "Replace to expensive" will not work. -# ProtocoLib may also be used if the server is in an "unsupported" version even if this option is disabled. -force_protocolib: false - configVersion: 1.15.5 lowMinecraftVersion: 1.21.11 diff --git a/defaultconfigs/1.21.9/config.yml b/defaultconfigs/1.21.9/config.yml index 87d03a9..91a796b 100644 --- a/defaultconfigs/1.21.9/config.yml +++ b/defaultconfigs/1.21.9/config.yml @@ -418,11 +418,5 @@ debug_log: false # Whether to show verbose debug logging debug_log_verbose: false -# In case something when wrong with CustomAnvil packet manager. -# If you see "missing class exception" or similar you may test this. -# If enabled and Protocolib absent or disabled "Replace to expensive" will not work. -# ProtocoLib may also be used if the server is in an "unsupported" version even if this option is disabled. -force_protocolib: false - configVersion: 1.11.0 lowMinecraftVersion: 1.21.9 diff --git a/defaultconfigs/1.21/config.yml b/defaultconfigs/1.21/config.yml index b6b620c..99606ba 100644 --- a/defaultconfigs/1.21/config.yml +++ b/defaultconfigs/1.21/config.yml @@ -406,10 +406,4 @@ debug_log: false # Whether to show verbose debug logging debug_log_verbose: false -# In case something when wrong with CustomAnvil packet manager. -# If you see "missing class exception" or similar you may test this. -# If enabled and Protocolib absent or disabled "Replace to expensive" will not work. -# ProtocoLib may also be used if the server is in an "unsupported" version even if this option is disabled. -force_protocolib: false - configVersion: 1.11.0 \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 95729b7..9006b0a 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -408,10 +408,4 @@ debug_log: false # Whether to show verbose debug logging debug_log_verbose: false -# In case something when wrong with CustomAnvil packet manager. -# If you see "missing class exception" or similar you may test this. -# If enabled and Protocolib absent or disabled "Replace to expensive" will not work. -# ProtocoLib may also be used if the server is in an "unsupported" version even if this option is disabled. -force_protocolib: false - configVersion: 1.11.0 From 26469982b210753f6ba3269c93d9e1c310c925d1 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 23 Mar 2026 18:26:20 +0100 Subject: [PATCH 071/166] indicate alias --- src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt index b06d07f..4c71c68 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/command/CASubCommand.kt @@ -16,7 +16,7 @@ abstract class CASubCommand: CommandExecutor { ): Boolean { if(!alreadySaid){ sender.sendMessage(ChatColor.RED.toString() + - "Please not that this command will be replaced as a subcommand of `/customanvil`") + "Please not that this command will be replaced as a subcommand of `/customanvil` or `/ca`") alreadySaid = true } From f0d2f07703d167259dc5183bd319e34c0b42a47a Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 23 Mar 2026 20:06:58 +0100 Subject: [PATCH 072/166] int item display better --- .../config/global/EnchantLimitConfigGui.java | 14 ++++- .../gui/config/settings/IntSettingsGui.java | 59 +++++++++++-------- 2 files changed, 45 insertions(+), 28 deletions(-) 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 index a95a5bb..e9edbeb 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantLimitConfigGui.java +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantLimitConfigGui.java @@ -58,9 +58,17 @@ public class EnchantLimitConfigGui extends AbstractEnchantConfigGui "Default (" + defaultValue + ")"; + case RESET -> String.valueOf(defaultValue); + default -> "Default"; + }; + + } + else return super.valueDisplayName(type, value); } }; } 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 index 5a713cf..f2ce0c1 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/IntSettingsGui.java +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/IntSettingsGui.java @@ -72,7 +72,8 @@ public class IntSettingsGui extends AbstractSettingGui { assert meta != null; meta.setDisplayName("§eReset to default value"); - meta.setLore(Collections.singletonList("§7Default value is §e" + holder.defaultVal)); + 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); @@ -86,42 +87,23 @@ public class IntSettingsGui extends AbstractSettingGui { * Update item using the setting value to match the new value. */ protected void updateValueDisplay() { - PatternPane pane = getPane(); - var displayNow = holder.valueDisplayName(now); // minus item GuiItem minusItem; if (now > holder.min) { - int planned = Math.max(holder.min, now - step); - ItemStack item = new ItemStack(Material.RED_TERRACOTTA); - ItemMeta meta = item.getItemMeta(); - assert meta != null; - - meta.setDisplayName("§e" + displayNow + " §f-> §e" + planned + " §r(§c-" + holder.valueDisplayName(now - planned) + "§r)"); - meta.setLore(Collections.singletonList(AbstractSettingGui.CLICK_LORE)); - item.setItemMeta(meta); - - minusItem = new GuiItem(item, updateNowConsumer(planned), CustomAnvil.instance); + int planned = Math.min(holder.max, now + step); + minusItem = valueEditItem(Material.RED_TERRACOTTA, ValueDisplayType.REMOVE, planned); } else { minusItem = GuiGlobalItems.backgroundItem(Material.BARRIER); } pane.bindItem('-', minusItem); //plus item - // may do a function to generalise ? GuiItem plusItem; if (now < holder.max) { int planned = Math.min(holder.max, now + step); - ItemStack item = new ItemStack(Material.GREEN_TERRACOTTA); - ItemMeta meta = item.getItemMeta(); - assert meta != null; - - meta.setDisplayName("§e" + displayNow + " §f-> §e" + planned + " §r(§a+" + holder.valueDisplayName(planned - now) + "§r)"); - meta.setLore(Collections.singletonList(AbstractSettingGui.CLICK_LORE)); - item.setItemMeta(meta); - - plusItem = new GuiItem(item, updateNowConsumer(planned), CustomAnvil.instance); + plusItem = valueEditItem(Material.GREEN_TERRACOTTA, ValueDisplayType.ADD, planned); } else { plusItem = GuiGlobalItems.backgroundItem(Material.BARRIER); } @@ -132,7 +114,7 @@ public class IntSettingsGui extends AbstractSettingGui { ItemMeta resultMeta = resultPaper.getItemMeta(); assert resultMeta != null; - resultMeta.setDisplayName("§fValue: §e" + displayNow); + resultMeta.setDisplayName("§fValue: §e" + holder.valueDisplayName(ValueDisplayType.CURRENT, now)); resultMeta.setLore(holder.displayLore); resultPaper.setItemMeta(resultMeta); @@ -150,7 +132,21 @@ public class IntSettingsGui extends AbstractSettingGui { } 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); } /** @@ -390,10 +386,23 @@ public class IntSettingsGui extends AbstractSettingGui { return getItem(itemMat, CasedStringUtil.detectToUpperSpacedCase(configPath)); } - public String valueDisplayName(int value) { + 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, } } From f3c6526967f594b27398a3c194d49da175c04b06 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 23 Mar 2026 23:54:38 +0100 Subject: [PATCH 073/166] add excellent enchant limit --- build.gradle.kts | 2 +- .../build.gradle.kts | 8 ++--- .../plugins/ExcellentEnchant5_3Registry.java | 0 .../ExcellentEnchant5_4EnchantSettings.java | 12 ++++++++ settings.gradle.kts | 4 +-- .../enchant/wrapped/CAEEV5_4Enchantment.java | 29 +++++++++++++++++++ .../packet/PacketManagerSelector.kt | 2 -- .../plugins/ExcellentEnchantsDependency.kt | 15 ++++++---- 8 files changed, 56 insertions(+), 16 deletions(-) rename impl/{ExcellentEnchant5_3 => ExcellentEnchant5_4}/build.gradle.kts (57%) rename impl/{ExcellentEnchant5_3 => ExcellentEnchant5_4}/src/main/java/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchant5_3Registry.java (100%) create mode 100644 impl/ExcellentEnchant5_4/src/main/java/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchant5_4EnchantSettings.java create mode 100644 src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5_4Enchantment.java diff --git a/build.gradle.kts b/build.gradle.kts index 2a7e922..6006fdc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -71,7 +71,7 @@ dependencies { compileOnly(project(":impl:LegacyEcoEnchant")) // ExcellentEnchants - implementation(project(":impl:ExcellentEnchant5_3")) + implementation(project(":impl:ExcellentEnchant5_4")) compileOnly("su.nightexpress.excellentenchants:Core:5.1.0") { exclude("org.spigotmc") } diff --git a/impl/ExcellentEnchant5_3/build.gradle.kts b/impl/ExcellentEnchant5_4/build.gradle.kts similarity index 57% rename from impl/ExcellentEnchant5_3/build.gradle.kts rename to impl/ExcellentEnchant5_4/build.gradle.kts index 7004edf..21335d5 100644 --- a/impl/ExcellentEnchant5_3/build.gradle.kts +++ b/impl/ExcellentEnchant5_4/build.gradle.kts @@ -11,11 +11,7 @@ repositories { } dependencies { - // Spigot api - compileOnly("org.spigotmc:spigot-api:1.18-R0.1-SNAPSHOT") - // Excellent Enchant - compileOnly("su.nightexpress.excellentenchants:Core:5.3.0") { - exclude("org.spigotmc") - } + compileOnly("su.nightexpress.excellentenchants:Core:5.4.1") + compileOnly("su.nightexpress.nightcore:main:2.14.1") } \ No newline at end of file diff --git a/impl/ExcellentEnchant5_3/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 similarity index 100% rename from impl/ExcellentEnchant5_3/src/main/java/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchant5_3Registry.java rename to impl/ExcellentEnchant5_4/src/main/java/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchant5_3Registry.java 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/settings.gradle.kts b/settings.gradle.kts index 81418f0..9de7d8c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,5 +18,5 @@ for (nmsPart in reobfNMS) { // compatibility subprojects include(":impl:LegacyEcoEnchant") findProject(":impl:LegacyEcoEnchant")?.name = "LegacyEcoEnchant" -include("impl:ExcellentEnchant5_3") -findProject(":impl:ExcellentEnchant5_3")?.name = "ExcellentEnchant5_3" \ No newline at end of file +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/enchant/wrapped/CAEEV5_4Enchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5_4Enchantment.java new file mode 100644 index 0000000..335430f --- /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.Material; +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 Material 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/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerSelector.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerSelector.kt index 775d197..4c3d680 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerSelector.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerSelector.kt @@ -1,10 +1,8 @@ package xyz.alexcrea.cuanvil.dependency.packet import org.bukkit.Bukkit -import su.nightexpress.nightcore.bridge.paper.PaperBridge import xyz.alexcrea.cuanvil.dependency.MinecraftVersionUtil import xyz.alexcrea.cuanvil.dependency.packet.versions.* -import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil import xyz.alexcrea.cuanvil.update.UpdateUtils object PacketManagerSelector { diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt index ebc4ad9..54ea2f4 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt @@ -11,6 +11,7 @@ import xyz.alexcrea.cuanvil.api.EnchantmentApi import xyz.alexcrea.cuanvil.api.event.listener.CATreatAnvilResultEvent 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.Method import su.nightexpress.excellentenchants.api.EnchantRegistry as V5EnchantRegistry @@ -25,6 +26,7 @@ import su.nightexpress.excellentenchants.registry.EnchantRegistry as PreV5Enchan 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"), @@ -70,14 +72,14 @@ class ExcellentEnchantsDependency { // As excellent enchants is loaded before custom anvil and register enchantment to registry, we need to unregister old "vanilla" enchant. when (listenerVersion) { - ListenerVersion.V5_3 -> { + ListenerVersion.V5_4 -> { for (enchantment in ExcellentEnchant5_3Registry.getRegistered()) { EnchantmentApi.unregisterEnchantment(enchantment.bukkitEnchantment.key) - EnchantmentApi.registerEnchantment(CAEEV5Enchantment(enchantment)) + EnchantmentApi.registerEnchantment(CAEEV5_4Enchantment(enchantment)) } } - ListenerVersion.V5 -> { + ListenerVersion.V5, ListenerVersion.V5_3 -> { for (enchantment in V5EnchantRegistry.getRegistered()) { EnchantmentApi.unregisterEnchantment(enchantment.bukkitEnchantment.key) EnchantmentApi.registerEnchantment(CAEEV5Enchantment(enchantment)) @@ -130,7 +132,8 @@ class ExcellentEnchantsDependency { when (listenerVersion) { ListenerVersion.V5, - ListenerVersion.V5_3 + ListenerVersion.V5_3, + ListenerVersion.V5_4, -> { if (listener is V5AnvilListener) { this.v5AnvilListener = listener @@ -165,6 +168,7 @@ class ExcellentEnchantsDependency { when (listenerVersion) { ListenerVersion.V5_3, ListenerVersion.V5, + ListenerVersion.V5_4, -> this.usedAnvilListener = v5AnvilListener!! ListenerVersion.PRE_V5 -> this.usedAnvilListener = preV5AnvilListener!! ListenerVersion.LEGACY -> this.usedAnvilListener = legacyAnvilListener!! @@ -228,7 +232,8 @@ class ExcellentEnchantsDependency { if (event.inventory.getItem(2) != null) { when (listenerVersion) { ListenerVersion.V5, - ListenerVersion.V5_3 + ListenerVersion.V5_3, + ListenerVersion.V5_4, -> v5AnvilListener!!.onClickAnvil(event) ListenerVersion.PRE_V5 -> preV5AnvilListener!!.onClickAnvil(event) ListenerVersion.LEGACY -> legacyAnvilListener!!.onClickAnvil(event) From 60ebdbf107bbab01057babe623b33f4c917551ac Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 24 Mar 2026 01:40:22 +0100 Subject: [PATCH 074/166] fix bad copy paste --- .../alexcrea/cuanvil/gui/config/settings/IntSettingsGui.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index f2ce0c1..56dcaf3 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/IntSettingsGui.java +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/IntSettingsGui.java @@ -102,7 +102,7 @@ public class IntSettingsGui extends AbstractSettingGui { //plus item GuiItem plusItem; if (now < holder.max) { - int planned = Math.min(holder.max, now + step); + int planned = Math.max(holder.min, now - step); plusItem = valueEditItem(Material.GREEN_TERRACOTTA, ValueDisplayType.ADD, planned); } else { plusItem = GuiGlobalItems.backgroundItem(Material.BARRIER); From 3d50e0ec8291fc21bc5584d00a1d58aac49ad330 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 24 Mar 2026 01:40:46 +0100 Subject: [PATCH 075/166] bruh --- .../alexcrea/cuanvil/gui/config/settings/IntSettingsGui.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 56dcaf3..73121a6 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/IntSettingsGui.java +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/IntSettingsGui.java @@ -92,7 +92,7 @@ public class IntSettingsGui extends AbstractSettingGui { // minus item GuiItem minusItem; if (now > holder.min) { - int planned = Math.min(holder.max, now + step); + int planned = Math.max(holder.min, now - step); minusItem = valueEditItem(Material.RED_TERRACOTTA, ValueDisplayType.REMOVE, planned); } else { minusItem = GuiGlobalItems.backgroundItem(Material.BARRIER); @@ -102,7 +102,7 @@ public class IntSettingsGui extends AbstractSettingGui { //plus item GuiItem plusItem; if (now < holder.max) { - int planned = Math.max(holder.min, now - step); + int planned = Math.min(holder.max, now + step); plusItem = valueEditItem(Material.GREEN_TERRACOTTA, ValueDisplayType.ADD, planned); } else { plusItem = GuiGlobalItems.backgroundItem(Material.BARRIER); From e5167971f403c79f6605ff9e00cf32778d6b3466 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 24 Mar 2026 02:47:06 +0100 Subject: [PATCH 076/166] [ci skip] faststat ver up --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 6006fdc..9453ce7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ dependencies { compileOnly("org.spigotmc:spigot-api:1.18-R0.1-SNAPSHOT") // fast stats - implementation("dev.faststats.metrics:bukkit:0.18.0") + implementation("dev.faststats.metrics:bukkit:0.19.0") // minimessage implementation("net.kyori:adventure-text-minimessage:4.25.0") From f520d5e3dbd80ba11dbdeb325bd4cbe71951b2dd Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:25:47 +0100 Subject: [PATCH 077/166] [ci skip] try better changelog --- .github/workflows/gradle.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 01518f4..9c76b3c 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -69,9 +69,7 @@ jobs: - name: Prepare release env variable if: ${{ github.event_name == 'release' && success() }} run: | - echo "RELEASE_CHANGELOG<> $GITHUB_ENV - echo "${{ github.event.release.body || '' }}" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV + printf 'RELEASE_CHANGELOG=%s\n' "${{ github.event.release.body || '' }}" >> $GITHUB_ENV echo "IS_GITHUB_PRERELEASE=${{ github.event.release.prerelease }}" >> $GITHUB_ENV - name: Prepare publish env variable @@ -168,4 +166,4 @@ jobs: # 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. - ${{ env.RELEASE_CHANGELOG }} \ No newline at end of file + ${{ env.RELEASE_CHANGELOG }} From 7fbf68dff3403b9abf6c6e38d294809e03d723ec Mon Sep 17 00:00:00 2001 From: alexcrea Date: Wed, 25 Mar 2026 03:30:50 +0100 Subject: [PATCH 078/166] [ci skip] add deprecation warning --- .../java/xyz/alexcrea/cuanvil/update/UpdateHandler.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/UpdateHandler.java b/src/main/java/xyz/alexcrea/cuanvil/update/UpdateHandler.java index 82ee0f7..660accb 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/update/UpdateHandler.java +++ b/src/main/java/xyz/alexcrea/cuanvil/update/UpdateHandler.java @@ -77,6 +77,12 @@ public class UpdateHandler { 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) { From d061bfc6f41622ca30b6e83c400ca1f836beea98 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Wed, 25 Mar 2026 03:52:18 +0100 Subject: [PATCH 079/166] Revert "[ci skip] try better changelog" This reverts commit f520d5e3dbd80ba11dbdeb325bd4cbe71951b2dd. --- .github/workflows/gradle.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 9c76b3c..01518f4 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -69,7 +69,9 @@ jobs: - name: Prepare release env variable if: ${{ github.event_name == 'release' && success() }} run: | - printf 'RELEASE_CHANGELOG=%s\n' "${{ github.event.release.body || '' }}" >> $GITHUB_ENV + echo "RELEASE_CHANGELOG<> $GITHUB_ENV + echo "${{ github.event.release.body || '' }}" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV echo "IS_GITHUB_PRERELEASE=${{ github.event.release.prerelease }}" >> $GITHUB_ENV - name: Prepare publish env variable @@ -166,4 +168,4 @@ jobs: # 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. - ${{ env.RELEASE_CHANGELOG }} + ${{ env.RELEASE_CHANGELOG }} \ No newline at end of file From 45fe037a925dbe3ae21c3bf1af4bd1d91aa3376b Mon Sep 17 00:00:00 2001 From: alexcrea Date: Wed, 25 Mar 2026 11:07:22 +0100 Subject: [PATCH 080/166] [ci skip] version bump --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 9453ce7..78f0724 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } group = "xyz.alexcrea" -version = "1.16.2" +version = "1.16.3" val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null val isPreRelease = System.getenv("IS_GITHUB_PRERELEASE") == "true" From 1a710863271c824e59713b54e166147effdc911b Mon Sep 17 00:00:00 2001 From: TrashyPixl Date: Thu, 26 Mar 2026 03:57:42 +0100 Subject: [PATCH 081/166] ci: use native github actions features for environment variables --- .github/workflows/gradle.yml | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 01518f4..bbc9f70 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -25,6 +25,10 @@ jobs: permissions: contents: write + env: + MODRINTH_VERSIONS: '["1.18.x", "1.19.x", "1.20.x", "1.21.x"]' + MODRINTH_PLATFORMS: '["spigot", "paper", "purpur", "folia"]' + steps: - uses: actions/checkout@v4 - name: Set up JDKs @@ -66,19 +70,6 @@ jobs: if: ${{ github.event_name != 'release' && success() }} run: echo "SMALL_COMMIT_HASH=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV - - name: Prepare release env variable - if: ${{ github.event_name == 'release' && success() }} - run: | - echo "RELEASE_CHANGELOG<> $GITHUB_ENV - echo "${{ github.event.release.body || '' }}" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - echo "IS_GITHUB_PRERELEASE=${{ github.event.release.prerelease }}" >> $GITHUB_ENV - - - name: Prepare publish env variable - run: | - echo MODRINTH_VERSIONS='["1.18.x", "1.19.x", "1.20.x", "1.21.x"]' >> $GITHUB_ENV - echo MODRINTH_PLATFORMS='["spigot", "paper", "purpur", "folia"]' >> $GITHUB_ENV - - name: Build with Gradle Wrapper run: ./gradlew build --parallel --stacktrace @@ -128,6 +119,7 @@ jobs: 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 @@ -156,7 +148,7 @@ jobs: game-versions: ${{ env.MODRINTH_VERSIONS }} channel: ${{ github.event.release.prerelease == false && 'release' || 'beta' }} files: build/libs/${{ env.ONLINE_JAR_NAME }} - changelog: ${{ env.RELEASE_CHANGELOG }} + changelog: ${{ github.event.release.body }} - name: Send release note to discord if: ${{ github.event_name == 'release' && github.repository_owner == 'alexcrea' && success() }} @@ -168,4 +160,4 @@ jobs: # 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. - ${{ env.RELEASE_CHANGELOG }} \ No newline at end of file + ${{ github.event.release.body }} From 7612eac7653efc0b69620686bae9e977dbe59eb5 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 3 Feb 2026 17:22:32 +0100 Subject: [PATCH 082/166] progress on using namespaced key instead of material --- build.gradle.kts | 8 ++- .../cuanvil/api/MaterialGroupApi.java | 9 +-- .../enchant/AdditionalTestEnchantment.java | 10 +-- .../enchant/wrapped/CAEEPreV5Enchantment.java | 7 ++- .../enchant/wrapped/CAEEV5Enchantment.java | 9 +-- .../cuanvil/enchant/wrapped/CAEcoEnchant.java | 7 ++- .../wrapped/CAIncompatibleAllEnchant.java | 5 +- .../wrapped/CALegacyEEEnchantment.java | 7 ++- .../enchant/wrapped/CALegacyEcoEnchant.java | 11 ++-- .../gui/config/SelectMaterialContainer.java | 13 ++-- .../gui/config/ask/SelectItemTypeGui.java | 3 +- .../elements/GroupConfigSubSettingGui.java | 13 ++-- .../settings/MaterialSelectSettingGui.java | 39 ++++++------ .../cuanvil/update/plugin/PUpdate_1_11_0.java | 13 +++- .../io/delilaheve/util/ConfigOptions.kt | 6 +- .../io/delilaheve/util/EnchantmentUtil.kt | 3 +- .../kotlin/io/delilaheve/util/ItemUtil.kt | 3 +- .../dependency/datapack/DataPackDependency.kt | 2 +- .../plugins/EcoItemDependencyUtil.kt | 34 +++++++++++ .../plugins/EnchantmentSquaredDependency.kt | 6 +- .../cuanvil/group/AbstractMaterialGroup.kt | 22 ++++--- .../cuanvil/group/EnchantConflictGroup.kt | 3 +- .../cuanvil/group/EnchantConflictManager.kt | 11 ++-- .../alexcrea/cuanvil/group/ExcludeGroup.kt | 15 ++--- .../alexcrea/cuanvil/group/IncludeGroup.kt | 15 ++--- .../cuanvil/group/ItemGroupManager.kt | 2 +- .../xyz/alexcrea/cuanvil/group/NegativeSet.kt | 51 ++++++++++++++++ .../cuanvil/listener/PrepareAnvilListener.kt | 17 +++--- .../cuanvil/recipe/AnvilCustomRecipe.kt | 7 +-- .../xyz/alexcrea/cuanvil/util/MaterialUtil.kt | 61 +++++++++++++++++++ .../alexcrea/cuanvil/util/UnitRepairUtil.kt | 5 +- .../cuanvil/api/MaterialGroupApiTests.java | 4 +- 32 files changed, 297 insertions(+), 124 deletions(-) create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/EcoItemDependencyUtil.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/group/NegativeSet.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/util/MaterialUtil.kt diff --git a/build.gradle.kts b/build.gradle.kts index 78f0724..49f0576 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -65,11 +65,15 @@ dependencies { // EnchantsSquaredRewritten compileOnly(files("libs/EnchantsSquared.jar")) - // EcoEnchants - compileOnly("com.willfp:EcoEnchants:12.11.1") + // 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") { diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/MaterialGroupApi.java b/src/main/java/xyz/alexcrea/cuanvil/api/MaterialGroupApi.java index 48dd500..cd71c7a 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/api/MaterialGroupApi.java +++ b/src/main/java/xyz/alexcrea/cuanvil/api/MaterialGroupApi.java @@ -3,6 +3,7 @@ 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; @@ -123,7 +124,7 @@ public class MaterialGroupApi { FileConfiguration config = ConfigHolder.ITEM_GROUP_HOLDER.getConfig(); String basePath = group.getName() + "."; - Set materialSet = group.getNonGroupInheritedMaterials(); + Set materialSet = group.getNonGroupInheritedMaterials(); Set groupSet = group.getGroups(); boolean empty = true; @@ -153,7 +154,7 @@ public class MaterialGroupApi { FileConfiguration config = ConfigHolder.ITEM_GROUP_HOLDER.getConfig(); String basePath = group.getName() + "."; - EnumSet materials = group.getMaterials(); + Set materials = group.getMaterials(); if (materials.isEmpty()) return false; @@ -163,8 +164,8 @@ public class MaterialGroupApi { return true; } - public static List materialSetToStringList(@NotNull Set materials) { - return materials.stream().map(material -> material.getKey().getKey().toLowerCase()).toList(); + public static List materialSetToStringList(@NotNull Set materials) { + return materials.stream().map(NamespacedKey::toString).toList(); } public static List materialGroupSetToStringList(@NotNull Set groups) { diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/AdditionalTestEnchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/AdditionalTestEnchantment.java index 832e5af..821838f 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/AdditionalTestEnchantment.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/AdditionalTestEnchantment.java @@ -1,6 +1,7 @@ package xyz.alexcrea.cuanvil.enchant; import org.bukkit.Material; +import org.bukkit.NamespacedKey; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; @@ -11,24 +12,23 @@ 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 itemMat Material of the tested 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 Material itemMat); - + @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 itemMat Material of the tested 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 Material itemMat, + @NotNull NamespacedKey itemType, @NotNull ItemStack item); } diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEPreV5Enchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEPreV5Enchantment.java index d3082c9..783798d 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEPreV5Enchantment.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEPreV5Enchantment.java @@ -1,6 +1,7 @@ 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; @@ -39,7 +40,7 @@ public class CAEEPreV5Enchantment extends CABukkitEnchantment implements Additio } @Override - public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull Material itemMat) { + public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) { if (!definition.hasConflicts()) return false; Set conflicts = definition.getConflicts(); @@ -52,8 +53,8 @@ public class CAEEPreV5Enchantment extends CABukkitEnchantment implements Additio } @Override - public boolean isItemConflict(@NotNull Map enchantments, @NotNull Material itemMat, @NotNull ItemStack item) { - if (Material.ENCHANTED_BOOK.equals(itemMat)) return false; + 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 index 813eda8..2d8f945 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java @@ -1,6 +1,7 @@ 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; @@ -27,7 +28,7 @@ public class CAEEV5Enchantment extends CABukkitEnchantment implements Additional } @Override - public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull Material itemMat) { + public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) { if (!hasConflicts()) return false; Set conflicts = getExclusiveSet(); @@ -41,10 +42,10 @@ public class CAEEV5Enchantment extends CABukkitEnchantment implements Additional } @Override - public boolean isItemConflict(@NotNull Map enchantments, @NotNull Material itemMat, @NotNull ItemStack item) { - if (Material.ENCHANTED_BOOK.equals(itemMat)) return false; + public boolean isItemConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType, @NotNull ItemStack item) { + if (Material.ENCHANTED_BOOK.getKey().equals(itemType)) return false; - String key = itemMat.getKey().getKey(); + String key = itemType.getKey(); ItemSet primary = eeenchantment.getPrimaryItems(); if (primary.getMaterials().contains(key)) return false; diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEcoEnchant.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEcoEnchant.java index b0e8fba..32d1346 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEcoEnchant.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEcoEnchant.java @@ -4,6 +4,7 @@ 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; @@ -23,7 +24,7 @@ public class CAEcoEnchant extends CABukkitEnchantment implements AdditionalTestE } @Override - public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull Material itemMat) { + public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) { if (enchantments.isEmpty()) return false; // Check if there is only self @@ -61,9 +62,9 @@ public class CAEcoEnchant extends CABukkitEnchantment implements AdditionalTestE @Override public boolean isItemConflict(@NotNull Map enchantments, - @NotNull Material itemMat, + @NotNull NamespacedKey itemType, @NotNull ItemStack item) { - if (Material.ENCHANTED_BOOK.equals(itemMat)) { + if (Material.ENCHANTED_BOOK.getKey().equals(itemType)) { return false; } diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAIncompatibleAllEnchant.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAIncompatibleAllEnchant.java index 218ce87..552ecd4 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAIncompatibleAllEnchant.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAIncompatibleAllEnchant.java @@ -1,6 +1,7 @@ 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; @@ -24,12 +25,12 @@ public class CAIncompatibleAllEnchant extends CABukkitEnchantment implements Add @Override - public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull Material itemMat) { + 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 Material itemMat, @NotNull ItemStack item) { + 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 index 191f8f3..74068d4 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEEEnchantment.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEEEnchantment.java @@ -1,6 +1,7 @@ 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; @@ -22,7 +23,7 @@ public class CALegacyEEEnchantment extends CABukkitEnchantment implements Additi } @Override - public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull Material itemMat) { + public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) { if (!eeenchantment.hasConflicts()) return false; Set conflicts = eeenchantment.getConflicts(); @@ -35,8 +36,8 @@ public class CALegacyEEEnchantment extends CABukkitEnchantment implements Additi } @Override - public boolean isItemConflict(@NotNull Map enchantments, @NotNull Material itemMat, @NotNull ItemStack item) { - if (Material.ENCHANTED_BOOK.equals(itemMat)) return false; + 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 index 3b4242d..cb24def 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEcoEnchant.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEcoEnchant.java @@ -4,12 +4,14 @@ 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; @@ -23,7 +25,7 @@ public class CALegacyEcoEnchant extends CABukkitEnchantment implements Additiona } @Override - public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull Material itemMat) { + public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) { if (enchantments.isEmpty()) return false; EnchantmentType type = this.ecoEnchant.getType(); @@ -48,14 +50,15 @@ public class CALegacyEcoEnchant extends CABukkitEnchantment implements Additiona @Override public boolean isItemConflict(@NotNull Map enchantments, - @NotNull Material itemMat, + @NotNull NamespacedKey itemType, @NotNull ItemStack item) { - if (Material.ENCHANTED_BOOK.equals(itemMat)) { + 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(itemMat)) { + if (target.getMaterials().contains(mat)) { return false; } } diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectMaterialContainer.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectMaterialContainer.java index 2f76694..3756341 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectMaterialContainer.java +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectMaterialContainer.java @@ -1,34 +1,35 @@ 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 { - EnumSet getSelectedMaterials(); + Set getSelectedMaterials(); - boolean setSelectedMaterials(EnumSet materials); + boolean setSelectedMaterials(Set materials); - EnumSet illegalMaterials(); + 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(); + 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(); + 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().name().toLowerCase()); + String formattedName = CasedStringUtil.snakeToUpperSpacedCase(materialIterator.next().getKey().toLowerCase()); groupLore.add("§7- §e" + formattedName); } 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 index b2d6afe..66411bd 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/SelectItemTypeGui.java +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/SelectItemTypeGui.java @@ -12,6 +12,7 @@ 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; @@ -52,7 +53,7 @@ public class SelectItemTypeGui extends AbstractAskGui { event.setCancelled(true); ItemStack cursor = event.getWhoClicked().getItemOnCursor(); - if(cursor.getType().isAir()) return; + if(MaterialUtil.INSTANCE.isAir(cursor)) return; ItemStack finalItem; if(materialOnly){ 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 index c49fec7..3d05674 100644 --- 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 @@ -5,6 +5,7 @@ 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; @@ -325,19 +326,19 @@ public class GroupConfigSubSettingGui extends MappedToListSubSettingGui implemen // ---------------------------- @Override - public EnumSet getSelectedMaterials() { + public Set getSelectedMaterials() { return this.group.getNonGroupInheritedMaterials(); } @Override - public boolean setSelectedMaterials(EnumSet materials) { + public boolean setSelectedMaterials(Set materials) { this.group.setNonGroupInheritedMaterials(materials); // Write to file configuration String[] groupNames = new String[materials.size()]; int index = 0; - for (Material otherGroup : materials) { - groupNames[index++] = otherGroup.name().toLowerCase(); + for (NamespacedKey otherGroup : materials) { + groupNames[index++] = otherGroup.getKey().toLowerCase(); } ConfigHolder.ITEM_GROUP_HOLDER.getConfig().set(this.group.getName()+"."+ItemGroupManager.MATERIAL_LIST_PATH, groupNames); @@ -353,8 +354,8 @@ public class GroupConfigSubSettingGui extends MappedToListSubSettingGui implemen } @Override - public EnumSet illegalMaterials() { - return EnumSet.of(Material.AIR); + public Set illegalMaterials() { + return Set.of(Material.AIR.getKey()); } // ---------------------------- 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 index a3963ce..fc519ff 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/MaterialSelectSettingGui.java +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/MaterialSelectSettingGui.java @@ -5,6 +5,7 @@ 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; @@ -18,18 +19,19 @@ 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 { +public class MaterialSelectSettingGui extends MappedElementListConfigGui { private final SelectMaterialContainer selector; private final Gui backGui; private boolean instantRemove; - private final List defaultMaterials; - private final EnumSet illegalMaterials; + private final List defaultMaterials; + private final Set illegalMaterials; private final int defaultMaterialHash; private int nowMaterialHash; @@ -161,8 +163,7 @@ public class MaterialSelectSettingGui extends MappedElementListConfigGui result = EnumSet.noneOf(Material.class); - result.addAll(this.elementGuiMap.keySet()); + Set result = new HashSet<>(this.elementGuiMap.keySet()); if(!this.selector.setSelectedMaterials(result)){ player.sendMessage("§cSomething went wrong while saving the change of value."); @@ -185,8 +186,8 @@ public class MaterialSelectSettingGui extends MappedElementListConfigGui getEveryDisplayableInstanceOfGeneric() { + protected Collection getEveryDisplayableInstanceOfGeneric() { return this.defaultMaterials; } @Override - protected void updateElement(Material material, GuiItem element) { + protected void updateElement(NamespacedKey material, GuiItem element) { // Nothing happen here I think } @Override - protected GuiItem newElementRequested(Material material, GuiItem newItem) { + protected GuiItem newElementRequested(NamespacedKey material, GuiItem newItem) { newItem.setAction(event -> { if(this.instantRemove){ removeMaterial(material); }else { - String materialName = CasedStringUtil.snakeToUpperSpacedCase(material.name().toLowerCase()); + String materialName = CasedStringUtil.snakeToUpperSpacedCase(material.getKey().toLowerCase()); // Create and show confirm remove gui. ConfirmActionGui confirmGui = new ConfirmActionGui( @@ -250,7 +251,7 @@ public class MaterialSelectSettingGui extends MappedElementListConfigGui materialList){ + private static int hashFromMaterialList(List materialList){ int defaultMaterialHash = 0; - for (Material material : materialList) { + for (NamespacedKey material : materialList) { defaultMaterialHash ^= material.hashCode(); } return defaultMaterialHash; 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 index 6d6baca..9740971 100644 --- 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 @@ -1,6 +1,7 @@ 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; @@ -11,6 +12,7 @@ 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; @@ -69,7 +71,12 @@ public class PUpdate_1_11_0 { // Create new group IncludeGroup group = new IncludeGroup(toolset); - group.addAll(toolMats); + 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); @@ -77,8 +84,8 @@ public class PUpdate_1_11_0 { if (tools == null) return; if (!(tools instanceof IncludeGroup include)) return; - List mats = List.of(toolMats); - Set matSet = include.getNonGroupInheritedMaterials(); + List mats = List.of(keys); + Set matSet = include.getNonGroupInheritedMaterials(); if (!matSet.containsAll(mats)) return; mats.forEach(matSet::remove); diff --git a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt index c69f2fb..b761784 100644 --- a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt +++ b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt @@ -348,7 +348,7 @@ object ConfigOptions { * * @return the current enchantment limit. -1 if none */ - fun getEnchantCountLimit(type: Material): Int? { + fun getEnchantCountLimit(type: NamespacedKey): Int? { val limit = materialEnchantCountLimit(type) if(limit != null) return limit @@ -362,8 +362,8 @@ object ConfigOptions { * * @return The current enchantment limit. -1 if none */ - private fun materialEnchantCountLimit(type: Material): Int? { - val path = "$ENCHANT_COUNT_LIMIT_ITEMS.${type.key.key.lowercase()}" + private fun materialEnchantCountLimit(type: NamespacedKey): Int? { + val path = "$ENCHANT_COUNT_LIMIT_ITEMS.${type.key.lowercase()}" if(!ConfigHolder.DEFAULT_CONFIG.config.isInt(path)) return null diff --git a/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt b/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt index b0fcc97..af959f2 100644 --- a/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt +++ b/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt @@ -6,6 +6,7 @@ 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 @@ -34,7 +35,7 @@ object EnchantmentUtil { val bypassFuse = player.hasPermission(CustomAnvil.bypassFusePermission) val bypassLevel = player.hasPermission(CustomAnvil.bypassLevelPermission) - var maxEnchantCount = ConfigOptions.getEnchantCountLimit(item.type) + var maxEnchantCount = ConfigOptions.getEnchantCountLimit(item.customType) if(maxEnchantCount == null || maxEnchantCount < 0) maxEnchantCount = Int.MAX_VALUE val allowed = other.filter { (enchantment, _) -> enchantment.isAllowed(player) } diff --git a/src/main/kotlin/io/delilaheve/util/ItemUtil.kt b/src/main/kotlin/io/delilaheve/util/ItemUtil.kt index a85af39..414d37e 100644 --- a/src/main/kotlin/io/delilaheve/util/ItemUtil.kt +++ b/src/main/kotlin/io/delilaheve/util/ItemUtil.kt @@ -4,6 +4,7 @@ import org.bukkit.Material.ENCHANTED_BOOK import org.bukkit.inventory.ItemStack import org.bukkit.inventory.meta.Damageable import xyz.alexcrea.cuanvil.enchant.CAEnchantment +import xyz.alexcrea.cuanvil.util.MaterialUtil.customType import kotlin.math.ceil import kotlin.math.max import kotlin.math.min @@ -90,5 +91,5 @@ object ItemUtil { */ fun ItemStack.canMergeWith( other: ItemStack? - ) = (other != null) && (type == other.type || (other.isEnchantedBook())) + ) = (other != null) && (customType == other.customType || (other.isEnchantedBook())) } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/datapack/DataPackDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/datapack/DataPackDependency.kt index f397200..b0e9e52 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/datapack/DataPackDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/datapack/DataPackDependency.kt @@ -145,7 +145,7 @@ object DataPackDependency { CustomAnvil.instance.logger.warning("Could not find material $name for item group $groupName") continue } - group.addToPolicy(mat) + group.addToPolicy(mat.key) } for (name in section.getStringList("groups")) { val otherGroup = MaterialGroupApi.getGroup(name) 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..14c7d9f --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/EcoItemDependencyUtil.kt @@ -0,0 +1,34 @@ +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 + } + +} \ 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 index f4da612..d769986 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/EnchantmentSquaredDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/EnchantmentSquaredDependency.kt @@ -102,15 +102,15 @@ class EnchantmentSquaredDependency(private val enchantmentSquaredPlugin: Plugin) private fun writeMissingGroups(){ // Write group that do not exist on custom anvil. val shield = IncludeGroup("shield") - shield.addToPolicy(Material.SHIELD) + shield.addToPolicy(Material.SHIELD.key) MaterialGroupApi.addMaterialGroup(shield) val elytra = IncludeGroup("elytra") - elytra.addToPolicy(Material.ELYTRA) + elytra.addToPolicy(Material.ELYTRA.key) MaterialGroupApi.addMaterialGroup(elytra) val trinkets = IncludeGroup("trinkets") - trinkets.addToPolicy(Material.ROTTEN_FLESH) + trinkets.addToPolicy(Material.ROTTEN_FLESH.key) MaterialGroupApi.addMaterialGroup(trinkets) } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/AbstractMaterialGroup.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/AbstractMaterialGroup.kt index ec6e7bc..d0d2bda 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/AbstractMaterialGroup.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/AbstractMaterialGroup.kt @@ -1,7 +1,8 @@ package xyz.alexcrea.cuanvil.group import org.bukkit.Material -import java.util.* +import org.bukkit.NamespacedKey +import xyz.alexcrea.cuanvil.util.MaterialUtil abstract class AbstractMaterialGroup(private val name: String) { protected val includedMaterial by lazy { createDefaultSet() } @@ -9,12 +10,12 @@ abstract class AbstractMaterialGroup(private val name: String) { /** * Get the group default set */ - protected abstract fun createDefaultSet(): EnumSet + protected abstract fun createDefaultSet(): MutableSet /** * Get if a material is allowed following the group policy */ - open fun contain(mat: Material): Boolean { + open fun contain(mat: NamespacedKey): Boolean { return mat in getMaterials() } @@ -27,13 +28,13 @@ abstract class AbstractMaterialGroup(private val name: String) { * Push a material to this group to follow this group policy * @return this instance. */ - abstract fun addToPolicy(mat: Material): AbstractMaterialGroup + 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: Material): AbstractMaterialGroup { + fun addAll(vararg materials: NamespacedKey): AbstractMaterialGroup { for (material in materials) { addToPolicy(material) } @@ -60,19 +61,19 @@ abstract class AbstractMaterialGroup(private val name: String) { /** * Get the group contained material as a set */ - abstract fun getMaterials(): EnumSet + abstract fun getMaterials(): Set /** * Get the group non-inherited material as a set */ - open fun getNonGroupInheritedMaterials(): EnumSet { + open fun getNonGroupInheritedMaterials(): Set { return includedMaterial } /** * Get the group non-inherited material as a set */ - open fun setNonGroupInheritedMaterials(materials: EnumSet) { + open fun setNonGroupInheritedMaterials(materials: Set) { this.includedMaterial.clear() this.includedMaterial.addAll(materials) } @@ -102,8 +103,9 @@ abstract class AbstractMaterialGroup(private val name: String) { // Test inner material val matIterator = includedMaterial.iterator() while (matIterator.hasNext()) { - val material = matIterator.next() - if (material.isAir) continue + val key = matIterator.next() + val material = MaterialUtil.getMatFromKey(key) + if (material == null || material.isAir) continue return material } // Test included group representative material diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt index 403d630..59841ac 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt @@ -2,6 +2,7 @@ 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( @@ -53,7 +54,7 @@ class EnchantConflictGroup( return canBypassByBeforeLevel(enchants) || canBypassByAfterLevel(enchants) } - fun allowed(enchants: Map, mat: Material): Boolean { + fun allowed(enchants: Map, mat: NamespacedKey): Boolean { if (enchantments.size < minBeforeBlock) { CustomAnvil.verboseLog("Conflicting bc of to many enchantments") return true diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt index f710f76..1dce406 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt @@ -8,6 +8,7 @@ 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 @@ -211,8 +212,8 @@ class EnchantConflictManager { item: ItemStack, newEnchant: CAEnchantment ): ConflictType { - val mat = item.type - CustomAnvil.verboseLog("Testing conflict for ${newEnchant.key} on ${mat.key}") + val type = item.customType + CustomAnvil.verboseLog("Testing conflict for ${newEnchant.key} on ${type}") val conflictList = newEnchant.conflicts var result = ConflictType.NO_CONFLICT @@ -223,7 +224,7 @@ class EnchantConflictManager { continue } - val allowed = conflict.allowed(appliedEnchants, mat) + val allowed = conflict.allowed(appliedEnchants, type) CustomAnvil.verboseLog("Was against $conflict and conflicting: ${!allowed} ") if (!allowed) { if (conflict.getEnchants().size <= 1) { @@ -239,7 +240,7 @@ class EnchantConflictManager { val immutableEnchants = Collections.unmodifiableMap(appliedEnchants) for (appliedEnchant in appliedEnchants.keys) { if (appliedEnchant is AdditionalTestEnchantment) { - val doConflict = appliedEnchant.isEnchantConflict(immutableEnchants, mat) + val doConflict = appliedEnchant.isEnchantConflict(immutableEnchants, type) if (doConflict) { CustomAnvil.verboseLog("Big conflict by additional test, stopping") return ConflictType.ENCHANTMENT_CONFLICT @@ -251,7 +252,7 @@ class EnchantConflictManager { if ((result != ConflictType.ITEM_CONFLICT) && (newEnchant is AdditionalTestEnchantment)) { val partialItem = createPartialResult(item, immutableEnchants) - if (newEnchant.isItemConflict(immutableEnchants, mat, partialItem)) { + if (newEnchant.isItemConflict(immutableEnchants, type, partialItem)) { return ConflictType.ITEM_CONFLICT } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/ExcludeGroup.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/ExcludeGroup.kt index 7684c3f..58ea48c 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/ExcludeGroup.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/ExcludeGroup.kt @@ -1,11 +1,12 @@ package xyz.alexcrea.cuanvil.group -import org.bukkit.Material +import org.bukkit.NamespacedKey import java.util.* class ExcludeGroup(name: String) : AbstractMaterialGroup(name) { - override fun createDefaultSet(): EnumSet { - return EnumSet.allOf(Material::class.java) + + override fun createDefaultSet(): MutableSet { + return NegativeSet(HashSet()) } private var includedGroup: MutableSet = HashSet() @@ -20,9 +21,9 @@ class ExcludeGroup(name: String) : AbstractMaterialGroup(name) { return false } - override fun addToPolicy(mat: Material): ExcludeGroup { - includedMaterial.remove(mat) - groupItems.remove(mat) + override fun addToPolicy(type: NamespacedKey): ExcludeGroup { + includedMaterial.remove(type) + groupItems.remove(type) return this } @@ -60,7 +61,7 @@ class ExcludeGroup(name: String) : AbstractMaterialGroup(name) { } } - override fun getMaterials(): EnumSet { + override fun getMaterials(): MutableSet { return groupItems } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/IncludeGroup.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/IncludeGroup.kt index 848789f..fc9614b 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/IncludeGroup.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/IncludeGroup.kt @@ -1,11 +1,12 @@ 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(): EnumSet { - return EnumSet.noneOf(Material::class.java) + override fun createDefaultSet(): MutableSet { + return HashSet() } private var includedGroup: MutableSet = HashSet() @@ -20,9 +21,9 @@ class IncludeGroup(name: String) : AbstractMaterialGroup(name) { return false } - override fun addToPolicy(mat: Material): IncludeGroup { - includedMaterial.add(mat) - groupItems.add(mat) + override fun addToPolicy(type: NamespacedKey): IncludeGroup { + includedMaterial.add(type) + groupItems.add(type) return this } @@ -47,7 +48,7 @@ class IncludeGroup(name: String) : AbstractMaterialGroup(name) { } } - override fun setNonGroupInheritedMaterials(materials: EnumSet) { + override fun setNonGroupInheritedMaterials(materials: Set) { super.setNonGroupInheritedMaterials(materials) updateMaterials() @@ -66,7 +67,7 @@ class IncludeGroup(name: String) : AbstractMaterialGroup(name) { } } - override fun getMaterials(): EnumSet { + override fun getMaterials(): MutableSet { return groupItems } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/ItemGroupManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/ItemGroupManager.kt index 65eef34..51b8249 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/ItemGroupManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/ItemGroupManager.kt @@ -91,7 +91,7 @@ class ItemGroupManager { } continue } - group.addToPolicy(material) + group.addToPolicy(material.key) } // Read group to include in this group policy. diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/NegativeSet.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/NegativeSet.kt new file mode 100644 index 0000000..386ba5f --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/NegativeSet.kt @@ -0,0 +1,51 @@ +package xyz.alexcrea.cuanvil.group + +class NegativeSet(val negate: MutableSet) : 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) + } + + 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 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/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index 0c4d2e7..4fe3034 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -24,6 +24,7 @@ import org.bukkit.inventory.meta.ItemMeta import xyz.alexcrea.cuanvil.dependency.DependencyManager import xyz.alexcrea.cuanvil.enchant.CAEnchantment import xyz.alexcrea.cuanvil.util.* +import xyz.alexcrea.cuanvil.util.MaterialUtil.isAir import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair import java.util.concurrent.atomic.AtomicInteger @@ -42,10 +43,6 @@ class PrepareAnvilListener : Listener { var IS_EMPTY_TEST = false } - private fun ItemStack?.isAir(): Boolean { - return this == null || this.type.isAir || this.amount == 0 - } - /** * Event handler logic for when an anvil contains items to be combined */ @@ -121,7 +118,7 @@ class PrepareAnvilListener : Listener { } private fun isImmutable(item: ItemStack?): Boolean { - if (item.isAir()) return false + if (item.isAir) return false val meta = item!!.itemMeta return meta != null && @@ -172,7 +169,7 @@ class PrepareAnvilListener : Listener { if (finalResult == null) return false event.result = finalResult.result - if (finalResult.result.isAir()) return false + if (finalResult.result.isAir) return false AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost, true) return true @@ -198,7 +195,7 @@ class PrepareAnvilListener : Listener { if (finalResult == null) return event.result = finalResult.result - if (finalResult.result.isAir()) return + if (finalResult.result.isAir) return AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost) } @@ -286,7 +283,7 @@ class PrepareAnvilListener : Listener { if (finalResult == null) return event.result = finalResult.result - if (finalResult.result.isAir()) return + if (finalResult.result.isAir) return AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost) } @@ -331,7 +328,7 @@ class PrepareAnvilListener : Listener { if (finalResult == null) return false event.result = finalResult.result - if (finalResult.result.isAir()) return false + if (finalResult.result.isAir) return false AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost) return true @@ -351,7 +348,7 @@ class PrepareAnvilListener : Listener { result = AnvilLoreEditUtil.tryLoreEditByPaper(player, first, second, xpCost) } - if (result.isAir() || first == result) { + if (result.isAir || first == result) { CustomAnvil.log("lore edit, But input is same as output") event.result = null return false diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/AnvilCustomRecipe.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/AnvilCustomRecipe.kt index fa1a977..4f83c14 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/AnvilCustomRecipe.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/AnvilCustomRecipe.kt @@ -7,6 +7,7 @@ import xyz.alexcrea.cuanvil.config.ConfigHolder import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant import xyz.alexcrea.cuanvil.util.AnvilUseType import xyz.alexcrea.cuanvil.util.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.MaterialUtil.isAir class AnvilCustomRecipe( val name: String, @@ -80,11 +81,7 @@ class AnvilCustomRecipe( } fun validate(): Boolean { - return (leftItem != null) && !(leftItem!!.type.isAir) && (leftItem!!.amount > 0) && - //(rightItem != null) && !(rightItem!!.type.isAir) && (rightItem!!.amount > 0) && - ((rightItem == null) || (!(rightItem!!.type.isAir) && (rightItem!!.amount > 0))) && - (resultItem != null) && !(resultItem!!.type.isAir) && (resultItem!!.amount > 0) - + return !leftItem.isAir && !rightItem.isAir && !resultItem.isAir } fun saveToFile(writeFile: Boolean, doBackup: Boolean) { 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..c4907c8 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MaterialUtil.kt @@ -0,0 +1,61 @@ +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.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 + } + + private val HasEcoItem = Bukkit.getPluginManager().isPluginEnabled("EcoItems") + + val ItemStack.customType: NamespacedKey + get() { + if(HasEcoItem) { + val result = EcoItemDependencyUtil.ecoItemNamespace(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(HasEcoItem) { + val result = EcoItemDependencyUtil.ecoItemMaterialFromKey(key) + if(result != null) return result + } + + return bukkitMaterialFromKey(key) + } + + fun itemFromKey(key: NamespacedKey): ItemStack { + if(HasEcoItem) { + val result = EcoItemDependencyUtil.newEcoItemstack(key) + if(result != null) return result + } + + return ItemStack(bukkitMaterialFromKey(key)!!) + } + + fun materialExist(key: NamespacedKey): Boolean { + return getMatFromKey(key) != null + } + + +} \ 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 index 2047e79..8463e27 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/UnitRepairUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/UnitRepairUtil.kt @@ -3,6 +3,7 @@ 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 { @@ -22,7 +23,7 @@ object UnitRepairUtil { if (other == null) return null val config = ConfigHolder.UNIT_REPAIR_HOLDER.config // Get configuration section if exist - val otherName = other.type.name.lowercase() + val otherName = other.customType.key.lowercase() var section = config.getConfigurationSection(otherName) if (section == null) { section = config.getConfigurationSection(otherName.uppercase()) @@ -44,7 +45,7 @@ object UnitRepairUtil { * 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.type.name.lowercase() + val itemName = item.customType.key.lowercase() val repairValue = if (section.isDouble(itemName)) { section.getDouble(itemName) } else if (section.isDouble(itemName.uppercase())) { diff --git a/src/test/java/xyz/alexcrea/cuanvil/api/MaterialGroupApiTests.java b/src/test/java/xyz/alexcrea/cuanvil/api/MaterialGroupApiTests.java index 164bc58..3fcafe7 100644 --- a/src/test/java/xyz/alexcrea/cuanvil/api/MaterialGroupApiTests.java +++ b/src/test/java/xyz/alexcrea/cuanvil/api/MaterialGroupApiTests.java @@ -15,7 +15,7 @@ public class MaterialGroupApiTests extends ConfigResetCustomAnvilTest { void groupAddAndRemove() { String groupName = "group"; IncludeGroup group = new IncludeGroup(groupName); - group.addToPolicy(Material.DIAMOND_PICKAXE); // We do not want it to be empty + group.addToPolicy(Material.DIAMOND_PICKAXE.getKey()); // We do not want it to be empty // Group not being set should not exist assertFalse(doGroupExist(groupName)); @@ -48,7 +48,7 @@ public class MaterialGroupApiTests extends ConfigResetCustomAnvilTest { void writeGroup_Reload() { String groupName = "group"; IncludeGroup group = new IncludeGroup(groupName); - group.addToPolicy(Material.DIAMOND_PICKAXE); // We do not want it to be empty + group.addToPolicy(Material.DIAMOND_PICKAXE.getKey()); // We do not want it to be empty // Group not being set should not exist assertFalse(doGroupExist(groupName)); From d4e94872d8403923c7c4bfd2aabc8f1cbc104112 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Wed, 4 Feb 2026 15:10:54 +0100 Subject: [PATCH 083/166] fix recipe error --- .../kotlin/xyz/alexcrea/cuanvil/recipe/AnvilCustomRecipe.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/AnvilCustomRecipe.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/AnvilCustomRecipe.kt index 4f83c14..57eb124 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/AnvilCustomRecipe.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/AnvilCustomRecipe.kt @@ -81,7 +81,9 @@ class AnvilCustomRecipe( } fun validate(): Boolean { - return !leftItem.isAir && !rightItem.isAir && !resultItem.isAir + return !leftItem.isAir && + (rightItem == null || !resultItem.isAir) && + !resultItem.isAir } fun saveToFile(writeFile: Boolean, doBackup: Boolean) { @@ -159,7 +161,7 @@ class AnvilCustomRecipe( CustomAnvil.verboseLog("Testing $name $leftItem") // We assume this function can be call only if leftItem != null - // Test is valid + // Test if valid if (!validate()) return false val leftSimilar = leftItem!!.isSimilar(item1) From daa1c6171fc1d9863cb2c3604a99859b94a7e408 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sun, 12 Apr 2026 20:39:31 +0200 Subject: [PATCH 084/166] check section exist for group & conflict --- .../alexcrea/cuanvil/group/EnchantConflictManager.kt | 11 ++++++++++- .../xyz/alexcrea/cuanvil/group/ItemGroupManager.kt | 11 ++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt index f710f76..670dea8 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt @@ -48,6 +48,11 @@ class EnchantConflictManager { 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() @@ -59,7 +64,11 @@ class EnchantConflictManager { val keys = config.getKeys(false) for (key in keys) { - val section = config.getConfigurationSection(key)!! + val section = config.getConfigurationSection(key) + if(section == null) { + warnBadKey(key) + continue + } val conflict = createConflict(section, itemManager, key) addConflict(conflict) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/ItemGroupManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/ItemGroupManager.kt index 65eef34..5c6cc9c 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/ItemGroupManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/ItemGroupManager.kt @@ -31,6 +31,8 @@ class ItemGroupManager { for (key in keys) { if (groupMap.containsKey(key)) continue + if (!config.isConfigurationSection(key)) + continue createGroup(config, keys, key) } } @@ -51,6 +53,7 @@ class ItemGroupManager { key: String ): AbstractMaterialGroup { val groupSection = config.getConfigurationSection(key)!! + val groupType = groupSection.getString(GROUP_TYPE_PATH, null) // Create Material group according to the group type @@ -105,11 +108,13 @@ class ItemGroupManager { continue } // Get other group or create it if not yet created - val otherGroup = if (!groupMap.containsKey(groupName)) { + val otherGroup = + if (!groupMap.containsKey(groupName)) { + if(!config.isConfigurationSection(groupName)) continue createGroup(config, keys, groupName) - } else { - groupMap[groupName]!! } + else groupMap[groupName]!! + // Avoid self reference or it will create an infinite loop if (otherGroup.isReferencing(group)) { CustomAnvil.instance.logger.warning( From b42fb42d83a030b3d45c9d8c9833c428f3a3fddd Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 21 Apr 2026 14:20:17 +0200 Subject: [PATCH 085/166] fix weird isAir error ? --- .../xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index 4fe3034..7126dd9 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -93,7 +93,7 @@ class PrepareAnvilListener : Listener { if (testCustomRecipe(event, inventory, player, first, second)) return // Test rename lonely item - val isAir = second.isAir() + val isAir = second.isAir CustomAnvil.verboseLog("checking air in main logic: $isAir") if (isAir) { doRenaming(event, inventory, player, first) From 12bfcb75cee90ca893c5e1209f865106813d6027 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 21 Apr 2026 15:12:33 +0200 Subject: [PATCH 086/166] add more spigot warnings --- .../cuanvil/gui/config/global/BasicConfigGui.java | 13 ++++++++++--- src/main/kotlin/io/delilaheve/CustomAnvil.kt | 5 +++++ .../cuanvil/dependency/MinecraftVersionUtil.kt | 5 +++++ 3 files changed, 20 insertions(+), 3 deletions(-) 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 index a07cb3c..97f8e54 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/BasicConfigGui.java +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/BasicConfigGui.java @@ -14,6 +14,7 @@ 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; @@ -282,9 +283,15 @@ public class BasicConfigGui extends ChestGui implements ValueUpdatableGui { 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 newer version of this plugin for this to work."); - lore.add("§cCurrently ProtocoLib is not detected."); + if(MinecraftVersionUtil.INSTANCE.isTooNewForSpigot()){ + lore.add(""); + lore.add("§4/!\\§cCaution§4/!\\ §cYou need ProtocoLib installed and working or a paper server."); + lore.add("§cCurrently ProtocoLib is not detected."); + } else { + lore.add(""); + lore.add("§4/!\\§cCaution§4/!\\ §cYou need ProtocoLib installed and working or a newer version of this plugin for this to work."); + lore.add("§cCurrently ProtocoLib is not detected."); + } } String[] loreAsArray = new String[lore.size()]; diff --git a/src/main/kotlin/io/delilaheve/CustomAnvil.kt b/src/main/kotlin/io/delilaheve/CustomAnvil.kt index 44ca0a8..3190878 100644 --- a/src/main/kotlin/io/delilaheve/CustomAnvil.kt +++ b/src/main/kotlin/io/delilaheve/CustomAnvil.kt @@ -11,6 +11,7 @@ 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.util.PlatformUtil import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry import xyz.alexcrea.cuanvil.gui.config.MainConfigGui @@ -194,6 +195,10 @@ open class CustomAnvil : JavaPlugin() { 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" diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/MinecraftVersionUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/MinecraftVersionUtil.kt index 69ec546..d981f81 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/MinecraftVersionUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/MinecraftVersionUtil.kt @@ -51,4 +51,9 @@ object MinecraftVersionUtil { } } + val isTooNewForSpigot: Boolean get() { + val versionParts = UpdateUtils.currentMinecraftVersionArray() + return versionParts[0] != 1 + } + } \ No newline at end of file From bce43649bc8865b2030cd7ed4bc78c2cae6eba8c Mon Sep 17 00:00:00 2001 From: alexcrea Date: Thu, 23 Apr 2026 12:28:30 +0200 Subject: [PATCH 087/166] fix build error --- .../alexcrea/cuanvil/enchant/wrapped/CAEEV5_4Enchantment.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index 335430f..a86877f 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5_4Enchantment.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5_4Enchantment.java @@ -1,6 +1,7 @@ package xyz.alexcrea.cuanvil.enchant.wrapped; import org.bukkit.Material; +import org.bukkit.NamespacedKey; import org.jetbrains.annotations.NotNull; import su.nightexpress.excellentenchants.api.enchantment.CustomEnchantment; import xyz.alexcrea.cuanvil.dependency.plugins.ExcellentEnchant5_4EnchantSettings; @@ -15,7 +16,7 @@ public class CAEEV5_4Enchantment extends CAEEV5Enchantment { } @Override - public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull Material itemMat) { + public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemMat) { if(super.isEnchantConflict(enchantments, itemMat)) return true; var limit = ExcellentEnchant5_4EnchantSettings.anvilLimit(); From 1b86996317d94b4399d61717137f31f46ed2726c Mon Sep 17 00:00:00 2001 From: alexcrea Date: Thu, 23 Apr 2026 12:32:41 +0200 Subject: [PATCH 088/166] add 26.1 ver to modrinth versions --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index bbc9f70..8c4a93c 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -26,7 +26,7 @@ jobs: contents: write env: - MODRINTH_VERSIONS: '["1.18.x", "1.19.x", "1.20.x", "1.21.x"]' + MODRINTH_VERSIONS: '["1.18.x", "1.19.x", "1.20.x", "1.21.x", "26.1.x"]' MODRINTH_PLATFORMS: '["spigot", "paper", "purpur", "folia"]' steps: From d867ca6c851065776bef93bf091fc1f161dbd377 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Thu, 23 Apr 2026 14:16:48 +0200 Subject: [PATCH 089/166] forgot paper version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 1a8f2e4..19e22ef 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,5 +9,5 @@ kotlin.daemon.jvmargs=-Xmx8G 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-1.21.11 +paperVersion=1.18-26.1.2 From 24db2594353cfc77f22f41ec2a89a2c69dffcabc Mon Sep 17 00:00:00 2001 From: alexcrea Date: Thu, 14 May 2026 06:28:50 +0200 Subject: [PATCH 090/166] add super enchant as lib --- libs/SuperEnchants-4.6.2-all.jar | Bin 0 -> 2238421 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 libs/SuperEnchants-4.6.2-all.jar diff --git a/libs/SuperEnchants-4.6.2-all.jar b/libs/SuperEnchants-4.6.2-all.jar new file mode 100644 index 0000000000000000000000000000000000000000..f4c832a1934d80afac3004e998b438d4aad6fc94 GIT binary patch literal 2238421 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

&)Jf{ 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 z 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

J(-U^V9>^U;HByxr!nqVJ0Va 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

T3&H6jqx%%y~TejdJ{@>nn{iIoG`W? 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

* + * @deprecated use #{@link #getCost()} instead * @return The current cost. */ + @Deprecated(forRemoval = true, since = "1.17.0") public int getLevelCost() { - return levelCost; + return cost.sum(); } /** @@ -123,9 +126,33 @@ public class CATreatAnvilResultEvent extends Event { *
  • Item rename
  • * * + * @deprecated use #{@link #getCost()} and set value on this instead * @param levelCost The new cost. */ + @Deprecated(forRemoval = true, since = "1.17.0") public void setLevelCost(int levelCost) { - this.levelCost = levelCost; + cost.setGeneric(levelCost - cost.getGeneric() - cost.sum()); } + + /** + * 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/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt index 0b5f396..4e59adc 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt @@ -30,6 +30,7 @@ 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.AnvilUseType +import xyz.alexcrea.cuanvil.util.AnvilXpUtil import xyz.alexcrea.cuanvil.util.MetricsUtil.trackError import java.util.logging.Level @@ -235,12 +236,12 @@ object DependencyManager { event: PrepareAnvilEvent, result: ItemStack, useType: AnvilUseType, - cost: Int + cost: AnvilXpUtil.AnvilCost ): CATreatAnvilResultEvent? { val treatEvent = CATreatAnvilResultEvent(event, useType, result, cost) try { unsafeTryTreatAnvilResult(treatEvent) - return treatEvent; + return treatEvent } catch (e: Exception) { logExceptionAndClear(event.view.player, event.inventory, e) return null diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt index 089a5fb..3dfba9c 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt @@ -16,6 +16,7 @@ import org.bukkit.inventory.AnvilInventory import org.bukkit.inventory.ItemStack import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener import xyz.alexcrea.cuanvil.util.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.AnvilXpUtil.AnvilCost import xyz.alexcrea.cuanvil.util.MetricsUtil.trackError import java.util.logging.Level import kotlin.reflect.KClass @@ -58,14 +59,14 @@ class DisenchantmentDependency { DisenchantEvent.onEvent(event) if (event.result != null) { CustomAnvil.log("Detected pre anvil item extract bypass.") - AnvilXpUtil.setAnvilInvXp(event.inventory, event.view, player, event.inventory.repairCost) + 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.setAnvilInvXp(event.inventory, event.view, player, event.inventory.repairCost) + AnvilXpUtil.setAnvilInvCost(event.inventory, event.view, player, AnvilCost(event.inventory.repairCost)) return true } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt index 6e7cf60..6b5f9c4 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt @@ -11,6 +11,7 @@ import valorless.havenbags.features.BagSkin import valorless.havenbags.features.BagUpgrade import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener import xyz.alexcrea.cuanvil.util.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.AnvilXpUtil.AnvilCost class HavenBagsDependency { @@ -53,14 +54,14 @@ class HavenBagsDependency { bagSkin.onPrepareAnvil(event) if (event.result != null) { CustomAnvil.log("Detected pre anvil heaven bag anvil skin.") - AnvilXpUtil.setAnvilInvXp(event.inventory, event.view, player, event.inventory.repairCost) + 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.setAnvilInvXp(event.inventory, event.view, player, event.inventory.repairCost) + AnvilXpUtil.setAnvilInvCost(event.inventory, event.view, player, AnvilCost(event.inventory.repairCost)) return true } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt index bc3afa4..1886589 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -25,6 +25,7 @@ import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe import xyz.alexcrea.cuanvil.util.AnvilLoreEditUtil import xyz.alexcrea.cuanvil.util.AnvilUseType import xyz.alexcrea.cuanvil.util.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.AnvilXpUtil.AnvilCost import xyz.alexcrea.cuanvil.util.CustomRecipeUtil import xyz.alexcrea.cuanvil.util.MiniMessageUtil import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair @@ -350,13 +351,13 @@ class AnvilResultListener : Listener { } private fun getFromLoreEditXpCost( - xpCost: AtomicInteger, + cost: AnvilCost, player: Player, inventory: AnvilInventory, ): Int { if (GameMode.CREATIVE == player.gameMode) return 0 - val repairCost = xpCost.get() + val repairCost = cost.sum() return if ((inventory.maximumRepairCost <= repairCost) || (player.level < repairCost) ) Int.MIN_VALUE @@ -376,9 +377,9 @@ class AnvilResultListener : Listener { val editType = AnvilLoreEditUtil.bookLoreEditIsAppend(leftItem, rightItem) ?: return false - val xpCost = AtomicInteger() + val cost = AnvilCost() if (editType) { - if (output != AnvilLoreEditUtil.handleLoreAppendByBook(player, leftItem, bookMeta, xpCost)) return false + if (output != AnvilLoreEditUtil.handleLoreAppendByBook(player, leftItem, bookMeta, cost)) return false // Remove pages to book val clearedBook: ItemStack? @@ -394,10 +395,10 @@ class AnvilResultListener : Listener { event, player, inventory, null, 0, clearedBook, 0, - output, getFromLoreEditXpCost(xpCost, player, inventory) + output, getFromLoreEditXpCost(cost, player, inventory) ) } else { - if (output != AnvilLoreEditUtil.handleLoreRemoveByBook(player, leftItem, xpCost)) return false + if (output != AnvilLoreEditUtil.handleLoreRemoveByBook(player, leftItem, cost)) return false // fill book meta val lore = DependencyManager.stripLore(leftItem) @@ -430,7 +431,7 @@ class AnvilResultListener : Listener { event, player, inventory, null, 0, rightCopy, 0, - output, getFromLoreEditXpCost(xpCost, player, inventory) + output, getFromLoreEditXpCost(cost, player, inventory) ) } } @@ -448,9 +449,9 @@ class AnvilResultListener : Listener { val editTypeIsAppend = AnvilLoreEditUtil.paperLoreEditIsAppend(leftItem, rightItem) ?: return false - val xpCost = AtomicInteger() + val cost = AnvilCost() if (editTypeIsAppend) { - if (output != AnvilLoreEditUtil.handleLoreAppendByPaper(player, leftItem, rightItem, xpCost)) return false + if (output != AnvilLoreEditUtil.handleLoreAppendByPaper(player, leftItem, rightItem, cost)) return false val paperCopy: ItemStack? if (LoreEditType.APPEND_PAPER.doConsume) { @@ -468,18 +469,18 @@ class AnvilResultListener : Listener { event, player, inventory, paperCopy, 0, rightItem, 1, - output, getFromLoreEditXpCost(xpCost, player, inventory) + output, getFromLoreEditXpCost(cost, player, inventory) ) } else { extractAnvilResult( event, player, inventory, null, 0, paperCopy, 0, - output, getFromLoreEditXpCost(xpCost, player, inventory) + output, getFromLoreEditXpCost(cost, player, inventory) ) } } else { - if (output != AnvilLoreEditUtil.handleLoreRemoveByPaper(player, leftItem, xpCost)) return false + if (output != AnvilLoreEditUtil.handleLoreRemoveByPaper(player, leftItem, cost)) return false val leftMeta = leftItem.itemMeta if (leftMeta == null || !leftMeta.hasLore()) return false @@ -512,14 +513,14 @@ class AnvilResultListener : Listener { event, player, inventory, rightClone, 0, rightItem, 1, - output, getFromLoreEditXpCost(xpCost, player, inventory) + output, getFromLoreEditXpCost(cost, player, inventory) ) } else { extractAnvilResult( event, player, inventory, null, 0, rightClone, 0, - output, getFromLoreEditXpCost(xpCost, player, inventory) + output, getFromLoreEditXpCost(cost, player, inventory) ) } } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index 0525a85..bf8d3dd 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -26,10 +26,10 @@ import xyz.alexcrea.cuanvil.dependency.DependencyManager import xyz.alexcrea.cuanvil.dialog.AnvilRenameDialog import xyz.alexcrea.cuanvil.enchant.CAEnchantment import xyz.alexcrea.cuanvil.util.* +import xyz.alexcrea.cuanvil.util.AnvilXpUtil.AnvilCost import xyz.alexcrea.cuanvil.util.MaterialUtil.isAir import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil -import java.util.concurrent.atomic.AtomicInteger /** * Listener for anvil events @@ -60,7 +60,7 @@ class PrepareAnvilListener : Listener { // 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.setAnvilInvXp(inventory, event.view, player, event.inventory.repairCost) + AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, AnvilCost(event.inventory.repairCost)) return } @@ -90,7 +90,7 @@ class PrepareAnvilListener : Listener { // 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.setAnvilInvXp(inventory, event.view, player, event.inventory.repairCost) + AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, AnvilCost(event.inventory.repairCost)) return } @@ -198,17 +198,17 @@ class PrepareAnvilListener : Listener { // Maybe add an option on custom craft to ignore/not ignore penalty ?? val xpCost = recipe.determineCost(amount, first, resultItem) - val levelCost = - if (recipe.removeExactLinearXp) AnvilXpUtil.calculateMinimumLevelForXp(xpCost) + val cost = AnvilCost() + cost.recipe = if (recipe.removeExactLinearXp) AnvilXpUtil.calculateMinimumLevelForXp(xpCost) else AnvilXpUtil.calculateLevelForXp(xpCost) - val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.CUSTOM_CRAFT, levelCost) + val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.CUSTOM_CRAFT, cost) if (finalResult == null) return false event.result = finalResult.result if (finalResult.result.isAir) return false - AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost, true) + AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost, true) return true } @@ -217,7 +217,8 @@ class PrepareAnvilListener : Listener { player: HumanEntity, first: ItemStack ) { val resultItem = DependencyManager.cloneItem(event, first) - var anvilCost = handleRename(resultItem, inventory, player) + val cost = AnvilCost() + cost.rename = handleRename(resultItem, inventory, player) // Test/stop if nothing changed. if (first == resultItem) { @@ -226,15 +227,15 @@ class PrepareAnvilListener : Listener { return } - anvilCost += AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.RENAME_ONLY) + cost.penalty = AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.RENAME_ONLY) - val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.RENAME_ONLY, anvilCost) + val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.RENAME_ONLY, cost) if (finalResult == null) return event.result = finalResult.result if (finalResult.result.isAir) return - AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost) + AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost) } private fun handleRename(resultItem: ItemStack, inventory: AnvilInventory, player: HumanEntity): Int { @@ -291,18 +292,18 @@ class PrepareAnvilListener : Listener { var hasChanged = !isIdentical(first.findEnchantments(), newEnchants) val resultItem = DependencyManager.cloneItem(event, first) - var anvilCost = 0 + val cost = AnvilCost() if(hasChanged){ resultItem.setEnchantmentsUnsafe(newEnchants) // Calculate enchantment cost - anvilCost+= AnvilXpUtil.getRightValues(second, resultItem) + cost.enchantment = AnvilXpUtil.getRightValues(second, resultItem) } // 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) - anvilCost += if (repaired) ConfigOptions.itemRepairCost else 0 + cost.repair = if (repaired) ConfigOptions.itemRepairCost else 0 hasChanged = hasChanged || repaired } @@ -313,18 +314,18 @@ class PrepareAnvilListener : Listener { return } // As calculatePenalty edit result, we need to calculate penalty after checking equality - anvilCost += AnvilXpUtil.calculatePenalty(first, second, resultItem, AnvilUseType.MERGE) + cost.penalty = AnvilXpUtil.calculatePenalty(first, second, resultItem, AnvilUseType.MERGE) // Calculate rename cost - anvilCost += handleRename(resultItem, inventory, player) + cost.rename = handleRename(resultItem, inventory, player) // Finally, we set result - val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.MERGE, anvilCost) + val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.MERGE, cost) if (finalResult == null) return event.result = finalResult.result if (finalResult.result.isAir) return - AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost) + AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost) } private fun isIdentical( @@ -347,14 +348,15 @@ class PrepareAnvilListener : Listener { val unitRepairAmount = first.getRepair(second) ?: return false val resultItem = DependencyManager.cloneItem(event, first) - var anvilCost = handleRename(resultItem, inventory, player) + val cost = AnvilCost() + cost.rename = handleRename(resultItem, inventory, player) val repairAmount = resultItem.unitRepair(second.amount, unitRepairAmount) if (repairAmount > 0) { - anvilCost += repairAmount * ConfigOptions.unitRepairCost + cost.repair = repairAmount * ConfigOptions.unitRepairCost } // We do not care about right item penalty for unit repair - anvilCost += AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.UNIT_REPAIR) + cost.penalty = AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.UNIT_REPAIR) // Test/stop if nothing changed. if (first == resultItem) { @@ -363,13 +365,13 @@ class PrepareAnvilListener : Listener { return true } - val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.UNIT_REPAIR, anvilCost) + val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.UNIT_REPAIR, cost) if (finalResult == null) return false event.result = finalResult.result if (finalResult.result.isAir) return false - AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost) + AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost) return true } @@ -380,11 +382,11 @@ class PrepareAnvilListener : Listener { val type = second.type var result: ItemStack? = null - val xpCost = AtomicInteger() + val cost = AnvilCost() if (Material.WRITABLE_BOOK == type) { - result = AnvilLoreEditUtil.tryLoreEditByBook(player, first, second, xpCost) + result = AnvilLoreEditUtil.tryLoreEditByBook(player, first, second, cost) } else if (Material.PAPER == type) { - result = AnvilLoreEditUtil.tryLoreEditByPaper(player, first, second, xpCost) + result = AnvilLoreEditUtil.tryLoreEditByPaper(player, first, second, cost) } if (result.isAir || first == result) { @@ -394,7 +396,7 @@ class PrepareAnvilListener : Listener { } event.result = result - AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, xpCost.get()) + AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost) return true } } \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilLoreEditUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilLoreEditUtil.kt index 36f0efb..85a0ee8 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilLoreEditUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilLoreEditUtil.kt @@ -8,10 +8,10 @@ import org.bukkit.permissions.Permissible 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.AnvilXpUtil.AnvilCost import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil import xyz.alexcrea.cuanvil.util.config.LoreEditType import java.util.* -import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference object AnvilLoreEditUtil { @@ -31,7 +31,7 @@ object AnvilLoreEditUtil { player: Permissible, first: ItemStack, book: BookMeta, - xpCost: AtomicInteger + cost: AnvilCost ): ItemStack? { if (!hasLoreEditByBookPermission(player)) return null @@ -53,14 +53,14 @@ object AnvilLoreEditUtil { if (result == first) return null // Handle xp - xpCost.addAndGet(colorCost) // Cost of using color - xpCost.addAndGet(outLines.size * LoreEditType.APPEND_BOOK.perLineCost) // per line cost - xpCost.addAndGet(baseEditLoreXpCost(first, result, LoreEditType.APPEND_BOOK)) // Fixed cost and work penalty + 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, xpCost: AtomicInteger): ItemStack? { + fun handleLoreRemoveByBook(player: Permissible, first: ItemStack, cost: AnvilCost): ItemStack? { if (!hasLoreEditByBookPermission(player)) return null // remove lore @@ -78,9 +78,9 @@ object AnvilLoreEditUtil { if (result == first) return null // Handle xp - xpCost.addAndGet(uncolorCost) - xpCost.addAndGet(currentLore.size * LoreEditType.REMOVE_BOOK.perLineCost) - xpCost.addAndGet(baseEditLoreXpCost(first, result, LoreEditType.REMOVE_BOOK)) + cost.lore = uncolorCost + cost.lore+= currentLore.size * LoreEditType.REMOVE_BOOK.perLineCost + baseEditLoreXpCost(cost, first, result, LoreEditType.REMOVE_BOOK) return result } @@ -116,12 +116,12 @@ object AnvilLoreEditUtil { return null } - fun tryLoreEditByBook(player: HumanEntity, first: ItemStack, second: ItemStack, xpCost: AtomicInteger): ItemStack? { + fun tryLoreEditByBook(player: HumanEntity, first: ItemStack, second: ItemStack, cost: AnvilCost): ItemStack? { val isAppend = bookLoreEditIsAppend(first, second) ?: return null val meta = second.itemMeta as BookMeta - return if (isAppend) handleLoreAppendByBook(player, first, meta, xpCost) - else handleLoreRemoveByBook(player, first, xpCost) + return if (isAppend) handleLoreAppendByBook(player, first, meta, cost) + else handleLoreRemoveByBook(player, first, cost) } // Return true if appended, false if removed, null if neither @@ -147,7 +147,7 @@ object AnvilLoreEditUtil { player: Permissible, first: ItemStack, second: ItemStack, - xpCost: AtomicInteger + cost: AnvilCost ): ItemStack? { if (!hasLoreEditByPaperPermission(player)) return null @@ -175,13 +175,13 @@ object AnvilLoreEditUtil { if (result == first) return null // Handle xp - xpCost.addAndGet(colorCost) - xpCost.addAndGet(baseEditLoreXpCost(first, result, LoreEditType.APPEND_PAPER)) + cost.lore = colorCost + baseEditLoreXpCost(cost, first, result, LoreEditType.APPEND_PAPER) return result } - fun handleLoreRemoveByPaper(player: Permissible, first: ItemStack, xpCost: AtomicInteger): ItemStack? { + fun handleLoreRemoveByPaper(player: Permissible, first: ItemStack, cost: AnvilCost): ItemStack? { if (!hasLoreEditByPaperPermission(player)) return null // remove lore line @@ -213,8 +213,8 @@ object AnvilLoreEditUtil { val uncolorCost = uncolorLine(player, line, LoreEditType.REMOVE_PAPER) // Handle other xp - xpCost.addAndGet(uncolorCost) - xpCost.addAndGet(baseEditLoreXpCost(first, result, LoreEditType.REMOVE_PAPER)) + cost.lore = uncolorCost + baseEditLoreXpCost(cost, first, result, LoreEditType.REMOVE_PAPER) return result } @@ -223,23 +223,23 @@ object AnvilLoreEditUtil { player: HumanEntity, first: ItemStack, second: ItemStack, - xpCost: AtomicInteger + cost: AnvilCost ): ItemStack? { val isAppend = paperLoreEditIsAppend(first, second) ?: return null - return if (isAppend) handleLoreAppendByPaper(player, first, second, xpCost) - else handleLoreRemoveByPaper(player, first, xpCost) + return if (isAppend) handleLoreAppendByPaper(player, first, second, cost) + else handleLoreRemoveByPaper(player, first, cost) } private fun baseEditLoreXpCost( + cost: AnvilCost, first: ItemStack, result: ItemStack, editType: LoreEditType - ): Int { - var xpCost = editType.fixedCost + ) { + cost.lore+= editType.fixedCost - xpCost += AnvilXpUtil.calculatePenalty(first, null, result, editType.useType) - return xpCost + cost.penalty = AnvilXpUtil.calculatePenalty(first, null, result, editType.useType) } fun colorPermission(player: Permissible, editType: LoreEditType): AnvilColorUtil.ColorPermissions { diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt index f60581f..5acc9b8 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt @@ -23,16 +23,55 @@ object AnvilXpUtil { const val EXCLUSIVE_PENALTY_PREFIX = "repair_cost" + class AnvilCost { + private val isAlone: Boolean + + var generic = 0 + var enchantment = 0 + var repair = 0 + var rename = 0 + var lore = 0 + var penalty = 0 + var recipe = 0 + + fun sum(): Int { + return generic + enchantment + repair + rename + lore + penalty + recipe + } + + constructor(generic: Int) { + this.generic = generic + isAlone = true + } + constructor() { + isAlone = false + } + } + + /** + * Display the required cost (either as xp or as ) + */ + fun setAnvilInvCost( + inventory: AnvilInventory, + view: InventoryView, + player: HumanEntity, + cost: AnvilCost, + ignoreRules: Boolean = false + ) { + // TODO check require money or xp cost & display appropriately + setAnvilInvXp(inventory, view, player, cost.sum(), ignoreRules) + } + /** * Display xp needed for the work on the anvil inventory */ - fun setAnvilInvXp( + private fun setAnvilInvXp( inventory: AnvilInventory, view: InventoryView, player: HumanEntity, anvilCost: Int, ignoreRules: Boolean = false ) { + // Test repair cost limit val finalAnvilCost = if ( !ignoreRules && @@ -78,7 +117,6 @@ object AnvilXpUtil { } player.updateInventory() - } } From 2c3e43cb846bd4fa60c2714c62352671f0deb24e Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sun, 24 May 2026 11:55:37 +0200 Subject: [PATCH 117/166] moved to vault unlocked --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 4effecb..36cc2d6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,7 +47,7 @@ repositories { } // For vault unlocked - maven { url = uri("https://jitpack.io") } + maven { url = uri("https://repo.codemc.io/repository/creatorfromhell/") } } val reobfNMS = providers.gradleProperty("subprojects.reobfnms") @@ -107,7 +107,7 @@ dependencies { compileOnly("dev.lone:api-itemsadder:4.0.10") // Vault api - compileOnly("com.github.MilkBowl:VaultAPI:1.7") + compileOnly("net.milkbowl.vault:VaultUnlockedAPI:2.16") // Include nms implementation(project(":nms:nms-common")) From 856c1e08bd3199ba31a663871f0cee6314a8b82e Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sun, 24 May 2026 20:47:26 +0200 Subject: [PATCH 118/166] add monetary config and generic progress --- defaultconfigs/1.18/config.yml | 19 ++++++++++ defaultconfigs/1.21.11/config.yml | 19 ++++++++++ defaultconfigs/1.21.9/config.yml | 19 ++++++++++ defaultconfigs/1.21/config.yml | 19 ++++++++++ src/main/kotlin/io/delilaheve/CustomAnvil.kt | 2 +- .../io/delilaheve/util/ConfigOptions.kt | 33 ++++++++++++++++ .../cuanvil/dependency/DependencyManager.kt | 4 +- .../dependency/econmy/EconomyManager.kt | 31 --------------- .../dependency/economy/EconomyManager.kt | 27 +++++++++++++ .../economy/UnlockedEconomyManager.kt | 35 +++++++++++++++++ .../dependency/economy/VaultEconomyManager.kt | 19 ++++++++++ .../cuanvil/listener/PrepareAnvilListener.kt | 38 +++++-------------- .../cuanvil/util/AnvilLoreEditUtil.kt | 2 +- .../xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt | 19 ++++------ src/main/resources/config.yml | 19 ++++++++++ 15 files changed, 230 insertions(+), 75 deletions(-) delete mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/dependency/econmy/EconomyManager.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/EconomyManager.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/UnlockedEconomyManager.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/VaultEconomyManager.kt diff --git a/defaultconfigs/1.18/config.yml b/defaultconfigs/1.18/config.yml index 12322e9..3edea26 100644 --- a/defaultconfigs/1.18/config.yml +++ b/defaultconfigs/1.18/config.yml @@ -430,6 +430,25 @@ lore_edit: 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 +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 diff --git a/defaultconfigs/1.21.11/config.yml b/defaultconfigs/1.21.11/config.yml index 39c8504..53ffabd 100644 --- a/defaultconfigs/1.21.11/config.yml +++ b/defaultconfigs/1.21.11/config.yml @@ -450,6 +450,25 @@ lore_edit: 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 +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 diff --git a/defaultconfigs/1.21.9/config.yml b/defaultconfigs/1.21.9/config.yml index 84eccba..40e558d 100644 --- a/defaultconfigs/1.21.9/config.yml +++ b/defaultconfigs/1.21.9/config.yml @@ -442,6 +442,25 @@ lore_edit: 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 +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 diff --git a/defaultconfigs/1.21/config.yml b/defaultconfigs/1.21/config.yml index df62e6a..87372a5 100644 --- a/defaultconfigs/1.21/config.yml +++ b/defaultconfigs/1.21/config.yml @@ -430,6 +430,25 @@ lore_edit: 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 +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 diff --git a/src/main/kotlin/io/delilaheve/CustomAnvil.kt b/src/main/kotlin/io/delilaheve/CustomAnvil.kt index 9548450..7f05fc6 100644 --- a/src/main/kotlin/io/delilaheve/CustomAnvil.kt +++ b/src/main/kotlin/io/delilaheve/CustomAnvil.kt @@ -12,7 +12,7 @@ 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.econmy.EconomyManager +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 diff --git a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt index 46f79bc..657fa38 100644 --- a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt +++ b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt @@ -72,6 +72,11 @@ object ConfigOptions { 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" @@ -110,6 +115,11 @@ object ConfigOptions { 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 @@ -625,4 +635,27 @@ object ConfigOptions { return false } + /* + * Monetary configs + */ + val shouldUseMoney: Boolean + get() { + return 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): Double { + return ConfigHolder.DEFAULT_CONFIG + .config + .getDouble("$MONETARY_MULTIPLIER_ROOT.$type", DEFAULT_MONEY_MULTIPLIER) + } + } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt index 4e59adc..69b0237 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt @@ -237,11 +237,11 @@ object DependencyManager { result: ItemStack, useType: AnvilUseType, cost: AnvilXpUtil.AnvilCost - ): CATreatAnvilResultEvent? { + ): ItemStack? { val treatEvent = CATreatAnvilResultEvent(event, useType, result, cost) try { unsafeTryTreatAnvilResult(treatEvent) - return treatEvent + return treatEvent.result } catch (e: Exception) { logExceptionAndClear(event.view.player, event.inventory, e) return null diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/econmy/EconomyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/econmy/EconomyManager.kt deleted file mode 100644 index d3b8c21..0000000 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/econmy/EconomyManager.kt +++ /dev/null @@ -1,31 +0,0 @@ -package xyz.alexcrea.cuanvil.dependency.econmy - -import net.milkbowl.vault.economy.Economy -import org.bukkit.OfflinePlayer -import org.bukkit.plugin.Plugin -import org.bukkit.plugin.RegisteredServiceProvider - -object EconomyManager { - - private var economy: Economy? = null - - fun setupEconomy(plugin: Plugin) { - if (economy != null) return - - if (plugin.server.pluginManager.getPlugin("Vault") == null) - return - - val rsp: RegisteredServiceProvider? = - plugin.server.servicesManager.getRegistration(Economy::class.java) - if (rsp == null) return - - economy = rsp.getProvider() - } - - fun has(player: OfflinePlayer, amount: Double): Boolean { - return economy?.has(player, amount) == true - } - - - -} 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..0777162 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/EconomyManager.kt @@ -0,0 +1,27 @@ +package xyz.alexcrea.cuanvil.dependency.economy + +import org.bukkit.plugin.Plugin + +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 + + + + +} 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..303c559 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/UnlockedEconomyManager.kt @@ -0,0 +1,35 @@ +package xyz.alexcrea.cuanvil.dependency.economy + +import net.milkbowl.vault2.economy.Economy +import org.bukkit.plugin.Plugin + +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 + } + + + +} \ 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..50837bf --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/VaultEconomyManager.kt @@ -0,0 +1,19 @@ +package xyz.alexcrea.cuanvil.dependency.economy + +import net.milkbowl.vault.economy.Economy +import org.bukkit.plugin.Plugin +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 + } + + +} \ 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 index bf8d3dd..00623d5 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -202,12 +202,7 @@ class PrepareAnvilListener : Listener { cost.recipe = if (recipe.removeExactLinearXp) AnvilXpUtil.calculateMinimumLevelForXp(xpCost) else AnvilXpUtil.calculateLevelForXp(xpCost) - val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.CUSTOM_CRAFT, cost) - if (finalResult == null) return false - - event.result = finalResult.result - if (finalResult.result.isAir) return false - + event.result = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.CUSTOM_CRAFT, cost) AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost, true) return true } @@ -227,14 +222,9 @@ class PrepareAnvilListener : Listener { return } - cost.penalty = AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.RENAME_ONLY) - - val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.RENAME_ONLY, cost) - if (finalResult == null) return - - event.result = finalResult.result - if (finalResult.result.isAir) return + cost.workPenalty = AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.RENAME_ONLY) + event.result = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.RENAME_ONLY, cost) AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost) } @@ -296,7 +286,7 @@ class PrepareAnvilListener : Listener { if(hasChanged){ resultItem.setEnchantmentsUnsafe(newEnchants) // Calculate enchantment cost - cost.enchantment = AnvilXpUtil.getRightValues(second, resultItem) + AnvilXpUtil.getRightValues(second, resultItem, cost) } // Calculate repair cost @@ -309,22 +299,17 @@ class PrepareAnvilListener : Listener { // Test/stop if nothing changed. if (!hasChanged) { - CustomAnvil.log("Mergable with second, But input is same as output") + CustomAnvil.log("Mergeable with second, But input is same as output") event.result = null return } // As calculatePenalty edit result, we need to calculate penalty after checking equality - cost.penalty = AnvilXpUtil.calculatePenalty(first, second, resultItem, AnvilUseType.MERGE) + cost.workPenalty = AnvilXpUtil.calculatePenalty(first, second, resultItem, AnvilUseType.MERGE) // Calculate rename cost cost.rename = handleRename(resultItem, inventory, player) // Finally, we set result - val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.MERGE, cost) - if (finalResult == null) return - - event.result = finalResult.result - if (finalResult.result.isAir) return - + event.result = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.MERGE, cost) AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost) } @@ -356,7 +341,7 @@ class PrepareAnvilListener : Listener { cost.repair = repairAmount * ConfigOptions.unitRepairCost } // We do not care about right item penalty for unit repair - cost.penalty = AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.UNIT_REPAIR) + cost.workPenalty = AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.UNIT_REPAIR) // Test/stop if nothing changed. if (first == resultItem) { @@ -365,12 +350,7 @@ class PrepareAnvilListener : Listener { return true } - val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.UNIT_REPAIR, cost) - if (finalResult == null) return false - - event.result = finalResult.result - if (finalResult.result.isAir) return false - + event.result = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.UNIT_REPAIR, cost) AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost) return true } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilLoreEditUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilLoreEditUtil.kt index 85a0ee8..16eca40 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilLoreEditUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilLoreEditUtil.kt @@ -239,7 +239,7 @@ object AnvilLoreEditUtil { ) { cost.lore+= editType.fixedCost - cost.penalty = AnvilXpUtil.calculatePenalty(first, null, result, editType.useType) + cost.workPenalty = AnvilXpUtil.calculatePenalty(first, null, result, editType.useType) } fun colorPermission(player: Permissible, editType: LoreEditType): AnvilColorUtil.ColorPermissions { diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt index 5acc9b8..6565166 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt @@ -31,11 +31,12 @@ object AnvilXpUtil { var repair = 0 var rename = 0 var lore = 0 - var penalty = 0 + var illegalPenalty = 0 + var workPenalty = 0 var recipe = 0 fun sum(): Int { - return generic + enchantment + repair + rename + lore + penalty + recipe + return generic + enchantment + repair + rename + lore + illegalPenalty + workPenalty + recipe } constructor(generic: Int) { @@ -197,10 +198,8 @@ object AnvilXpUtil { * Function to calculate right enchantment values * it include enchantment placed on final item and conflicting enchantment */ - fun getRightValues(right: ItemStack, result: ItemStack): Int { + fun getRightValues(right: ItemStack, result: ItemStack, cost: AnvilCost) { // Calculate right value and illegal enchant penalty - var illegalPenalty = 0 - var rightValue = 0 val rightIsFormBook = right.isEnchantedBook() val resultEnchs = result.findEnchantments() @@ -218,7 +217,7 @@ object AnvilXpUtil { resultEnchsKeys.remove(enchantment.key) if (ConflictType.ENCHANTMENT_CONFLICT == conflictType) { - illegalPenalty += ConfigOptions.sacrificeIllegalCost + cost.illegalPenalty += ConfigOptions.sacrificeIllegalCost CustomAnvil.verboseLog("Big conflict. Adding illegal price penalty") } continue @@ -229,16 +228,14 @@ object AnvilXpUtil { 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)") - rightValue += value + cost.enchantment += value } CustomAnvil.log( "Calculated right values: " + - "rightValue: $rightValue, " + - "illegalPenalty: $illegalPenalty" + "rightValue: ${cost.enchantment}, " + + "illegalPenalty: ${cost.illegalPenalty}" ) - - return rightValue + illegalPenalty } /** diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 49b5e5a..f5e2c5a 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -432,6 +432,25 @@ lore_edit: 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 +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 From 1660250ee1e40f3bdbc33470611ff15571f5ccb4 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sun, 24 May 2026 21:27:17 +0200 Subject: [PATCH 119/166] monetary dependency functions --- .../io/delilaheve/util/ConfigOptions.kt | 8 +++-- .../dependency/economy/EconomyManager.kt | 7 +++- .../economy/UnlockedEconomyManager.kt | 35 +++++++++++++++++++ .../dependency/economy/VaultEconomyManager.kt | 18 ++++++++++ 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt index 657fa38..487bb87 100644 --- a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt +++ b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt @@ -7,6 +7,7 @@ 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.AnvilUseType import java.util.* @@ -640,9 +641,10 @@ object ConfigOptions { */ val shouldUseMoney: Boolean get() { - return ConfigHolder.DEFAULT_CONFIG - .config - .getBoolean(SHOULD_USE_MONEY, DEFAULT_SHOULD_USE_MONEY) + return EconomyManager.economy?.initialized() == true && + ConfigHolder.DEFAULT_CONFIG + .config + .getBoolean(SHOULD_USE_MONEY, DEFAULT_SHOULD_USE_MONEY) } val usedCurrency: String diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/EconomyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/EconomyManager.kt index 0777162..dc1038a 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/EconomyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/EconomyManager.kt @@ -1,6 +1,8 @@ package xyz.alexcrea.cuanvil.dependency.economy +import org.bukkit.entity.Player import org.bukkit.plugin.Plugin +import java.math.BigDecimal interface EconomyManager { @@ -21,7 +23,10 @@ interface EconomyManager { 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 index 303c559..9bea81c 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/UnlockedEconomyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/UnlockedEconomyManager.kt @@ -1,7 +1,10 @@ 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 { @@ -30,6 +33,38 @@ class UnlockedEconomyManager: EconomyManager { 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 index 50837bf..79a8036 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/VaultEconomyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/VaultEconomyManager.kt @@ -1,7 +1,10 @@ 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? @@ -15,5 +18,20 @@ class VaultEconomyManager : EconomyManager { 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 From ac9f4921258b8a952be913b7066b71bb79c550e1 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 25 May 2026 17:50:02 +0200 Subject: [PATCH 120/166] monetary minimum version & rename impl --- defaultconfigs/1.18/config.yml | 2 ++ defaultconfigs/1.21.11/config.yml | 2 ++ defaultconfigs/1.21.9/config.yml | 2 ++ defaultconfigs/1.21/config.yml | 2 ++ .../xyz/alexcrea/cuanvil/util/RenameAnvilUtil.kt | 12 ++++++++++++ .../kotlin/io/delilaheve/util/ConfigOptions.kt | 15 +++++++++++---- src/main/resources/config.yml | 3 +++ 7 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/RenameAnvilUtil.kt diff --git a/defaultconfigs/1.18/config.yml b/defaultconfigs/1.18/config.yml index 3edea26..1a1663a 100644 --- a/defaultconfigs/1.18/config.yml +++ b/defaultconfigs/1.18/config.yml @@ -432,6 +432,8 @@ lore_edit: # Allow to replace the xp cost by a monetary cost # If enabled it will not be bound to the experience level limits +# +# This feature can only be enabled starting with 1.21 monetary_cost: enabled: false # If using vault unlocked this allow to specify what currency should be used for anvil usage diff --git a/defaultconfigs/1.21.11/config.yml b/defaultconfigs/1.21.11/config.yml index 53ffabd..2daa373 100644 --- a/defaultconfigs/1.21.11/config.yml +++ b/defaultconfigs/1.21.11/config.yml @@ -452,6 +452,8 @@ lore_edit: # Allow to replace the xp cost by a monetary cost # If enabled it will not be bound to the experience level limits +# +# This feature can only be enabled starting with 1.21 monetary_cost: enabled: false # If using vault unlocked this allow to specify what currency should be used for anvil usage diff --git a/defaultconfigs/1.21.9/config.yml b/defaultconfigs/1.21.9/config.yml index 40e558d..8532881 100644 --- a/defaultconfigs/1.21.9/config.yml +++ b/defaultconfigs/1.21.9/config.yml @@ -444,6 +444,8 @@ lore_edit: # Allow to replace the xp cost by a monetary cost # If enabled it will not be bound to the experience level limits +# +# This feature can only be enabled starting with 1.21 monetary_cost: enabled: false # If using vault unlocked this allow to specify what currency should be used for anvil usage diff --git a/defaultconfigs/1.21/config.yml b/defaultconfigs/1.21/config.yml index 87372a5..25e47dc 100644 --- a/defaultconfigs/1.21/config.yml +++ b/defaultconfigs/1.21/config.yml @@ -432,6 +432,8 @@ lore_edit: # Allow to replace the xp cost by a monetary cost # If enabled it will not be bound to the experience level limits +# +# This feature can only be enabled starting with 1.21 monetary_cost: enabled: false # If using vault unlocked this allow to specify what currency should be used for anvil usage diff --git a/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/RenameAnvilUtil.kt b/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/RenameAnvilUtil.kt new file mode 100644 index 0000000..279016b --- /dev/null +++ b/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/RenameAnvilUtil.kt @@ -0,0 +1,12 @@ +package xyz.alexcrea.cuanvil.util + +import org.bukkit.inventory.InventoryView + +// TODO yet another cleanup to do on legacy removal branch +object RenameAnvilUtil { + + fun rename(view: InventoryView, name: String) { + view.title = name + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt index 487bb87..ddaa348 100644 --- a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt +++ b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt @@ -9,7 +9,10 @@ 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.update.UpdateUtils +import xyz.alexcrea.cuanvil.update.Version import xyz.alexcrea.cuanvil.util.AnvilUseType +import java.math.BigDecimal import java.util.* /** @@ -87,6 +90,9 @@ object ConfigOptions { const val DEBUG_LOGGING = "debug_log" const val VERBOSE_DEBUG_LOGGING = "debug_log_verbose" + // Minimum versions + val MINIMUM_MONETARY_COST_VER = Version(21, 0, 0) + // ---------------------- // Default config values // ---------------------- @@ -637,11 +643,12 @@ object ConfigOptions { } /* - * Monetary configs + * Monetary configs (only for 1.21+) */ val shouldUseMoney: Boolean get() { return EconomyManager.economy?.initialized() == true && + UpdateUtils.currentMinecraftVersion().greaterEqual(MINIMUM_MONETARY_COST_VER) && ConfigHolder.DEFAULT_CONFIG .config .getBoolean(SHOULD_USE_MONEY, DEFAULT_SHOULD_USE_MONEY) @@ -654,10 +661,10 @@ object ConfigOptions { .getString(MONEY_CURRENCY, DEFAULT_MONEY_CURRENCY)!! } - fun getMonetaryMultiplier(type: String): Double { - return ConfigHolder.DEFAULT_CONFIG + fun getMonetaryMultiplier(type: String): BigDecimal { + return BigDecimal(ConfigHolder.DEFAULT_CONFIG .config - .getDouble("$MONETARY_MULTIPLIER_ROOT.$type", DEFAULT_MONEY_MULTIPLIER) + .getDouble("$MONETARY_MULTIPLIER_ROOT.$type", DEFAULT_MONEY_MULTIPLIER)) } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index f5e2c5a..5ba61ec 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -434,6 +434,8 @@ lore_edit: # Allow to replace the xp cost by a monetary cost # If enabled it will not be bound to the experience level limits +# +# This feature can only be enabled starting with 1.21 monetary_cost: enabled: false # If using vault unlocked this allow to specify what currency should be used for anvil usage @@ -448,6 +450,7 @@ monetary_cost: 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 From 1b3447d0416fff9994fff3521da88ebbefc36087 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 25 May 2026 18:05:29 +0200 Subject: [PATCH 121/166] monetary cost display --- .../{RenameAnvilUtil.kt => AnvilTitleUtil.kt} | 2 +- .../io/delilaheve/util/ConfigOptions.kt | 2 +- .../cuanvil/dependency/DependencyManager.kt | 5 +- .../dependency/economy/EconomyManager.kt | 4 +- .../economy/UnlockedEconomyManager.kt | 18 +++-- .../dependency/economy/VaultEconomyManager.kt | 4 +- .../plugins/DisenchantmentDependency.kt | 4 +- .../dependency/plugins/HavenBagsDependency.kt | 4 +- .../cuanvil/listener/PrepareAnvilListener.kt | 14 ++-- .../xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt | 69 +++++++++++++++++-- 10 files changed, 97 insertions(+), 29 deletions(-) rename nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/{RenameAnvilUtil.kt => AnvilTitleUtil.kt} (89%) diff --git a/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/RenameAnvilUtil.kt b/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilTitleUtil.kt similarity index 89% rename from nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/RenameAnvilUtil.kt rename to nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilTitleUtil.kt index 279016b..805b150 100644 --- a/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/RenameAnvilUtil.kt +++ b/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilTitleUtil.kt @@ -3,7 +3,7 @@ package xyz.alexcrea.cuanvil.util import org.bukkit.inventory.InventoryView // TODO yet another cleanup to do on legacy removal branch -object RenameAnvilUtil { +object AnvilTitleUtil { fun rename(view: InventoryView, name: String) { view.title = name diff --git a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt index ddaa348..9ee7317 100644 --- a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt +++ b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt @@ -91,7 +91,7 @@ object ConfigOptions { const val VERBOSE_DEBUG_LOGGING = "debug_log_verbose" // Minimum versions - val MINIMUM_MONETARY_COST_VER = Version(21, 0, 0) + val MINIMUM_MONETARY_COST_VER = Version(1, 21, 0) // ---------------------- // Default config values diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt index 69b0237..418dbf1 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt @@ -7,6 +7,7 @@ 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 @@ -199,7 +200,7 @@ object DependencyManager { } // Return true if should bypass (either by a dependency or error) - fun tryEventPreAnvilBypass(event: PrepareAnvilEvent, player: HumanEntity): Boolean { + fun tryEventPreAnvilBypass(event: PrepareAnvilEvent, player: Player): Boolean { try { return unsafeTryEventPreAnvilBypass(event, player) } catch (e: Exception) { @@ -208,7 +209,7 @@ object DependencyManager { } } - private fun unsafeTryEventPreAnvilBypass(event: PrepareAnvilEvent, player: HumanEntity): Boolean { + private fun unsafeTryEventPreAnvilBypass(event: PrepareAnvilEvent, player: Player): Boolean { // Run the event val bypassEvent = CAPreAnvilBypassEvent(event) Bukkit.getPluginManager().callEvent(bypassEvent) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/EconomyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/EconomyManager.kt index dc1038a..67a6c1e 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/EconomyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/EconomyManager.kt @@ -12,10 +12,10 @@ interface EconomyManager { fun setupEconomy(plugin: Plugin) { if (plugin.server.pluginManager.getPlugin("Vault") == null) return - if(UnlockedEconomyManager.unlockedAvailable()) + if (UnlockedEconomyManager.unlockedAvailable()) economy = UnlockedEconomyManager(plugin) - if(economy == null || !economy!!.initialized()) + if (economy == null || !economy!!.initialized()) economy = VaultEconomyManager(plugin) } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/UnlockedEconomyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/UnlockedEconomyManager.kt index 9bea81c..da66f30 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/UnlockedEconomyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/UnlockedEconomyManager.kt @@ -6,7 +6,7 @@ import org.bukkit.entity.Player import org.bukkit.plugin.Plugin import java.math.BigDecimal -class UnlockedEconomyManager: EconomyManager { +class UnlockedEconomyManager : EconomyManager { val plugin: String val economy: Economy? @@ -42,23 +42,27 @@ class UnlockedEconomyManager: EconomyManager { } override fun has(player: Player, money: BigDecimal): Boolean { - if(money.signum() <= 0) return true + if (money.signum() <= 0) return true - return economy!!.has(plugin, + return economy!!.has( + plugin, player.uniqueId, player.world.name, currency(), - money) + money + ) } override fun remove(player: Player, money: BigDecimal): Boolean { - if(money.signum() <= 0) return true + if (money.signum() <= 0) return true - return economy!!.withdraw(plugin, + return economy!!.withdraw( + plugin, player.uniqueId, player.world.name, currency(), - money) + money + ) .transactionSuccess() } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/VaultEconomyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/VaultEconomyManager.kt index 79a8036..058485e 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/VaultEconomyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/economy/VaultEconomyManager.kt @@ -19,13 +19,13 @@ class VaultEconomyManager : EconomyManager { } override fun has(player: Player, money: BigDecimal): Boolean { - if(money.signum() <= 0) return true + 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 + if (money.signum() <= 0) return true return economy!!.withdrawPlayer(player, money.toDouble()).transactionSuccess() } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt index 3dfba9c..7a7a5eb 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt @@ -8,7 +8,7 @@ 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.HumanEntity +import org.bukkit.entity.Player import org.bukkit.event.Listener import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.event.inventory.PrepareAnvilEvent @@ -51,7 +51,7 @@ class DisenchantmentDependency { InventoryClickEvent.getHandlerList().unregister(listener) } - fun testPrepareAnvil(event: PrepareAnvilEvent, player: HumanEntity): Boolean { + fun testPrepareAnvil(event: PrepareAnvilEvent, player: Player): Boolean { val previousResult = event.result event.result = null diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt index 6b5f9c4..b2e7ef4 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt @@ -1,7 +1,7 @@ package xyz.alexcrea.cuanvil.dependency.plugins import io.delilaheve.CustomAnvil -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 @@ -46,7 +46,7 @@ class HavenBagsDependency { } - fun testPrepareAnvil(event: PrepareAnvilEvent, player: HumanEntity): Boolean { + fun testPrepareAnvil(event: PrepareAnvilEvent, player: Player): Boolean { val previousResult = event.result event.result = null diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index 00623d5..c71e82e 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -13,6 +13,7 @@ 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.event.EventHandler import org.bukkit.event.EventPriority import org.bukkit.event.Listener @@ -54,7 +55,8 @@ class PrepareAnvilListener : Listener { @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) fun anvilCombineCheck(event: PrepareAnvilEvent) { // Should find player - val player: HumanEntity = InventoryViewUtil.getInstance().getPlayer(event.view) + val player = InventoryViewUtil.getInstance().getPlayer(event.view) + if(player !is Player) return val inventory = event.inventory // Test if custom anvil is bypassed before immutability test @@ -183,7 +185,7 @@ class PrepareAnvilListener : Listener { // return true if a custom recipe exist with these ingredients private fun testCustomRecipe( event: PrepareAnvilEvent, inventory: AnvilInventory, - player: HumanEntity, + player: Player, first: ItemStack, second: ItemStack? ): Boolean { val recipe = CustomRecipeUtil.getCustomRecipe(first, second) @@ -209,7 +211,7 @@ class PrepareAnvilListener : Listener { private fun doRenaming( event: PrepareAnvilEvent, inventory: AnvilInventory, - player: HumanEntity, first: ItemStack + player: Player, first: ItemStack ) { val resultItem = DependencyManager.cloneItem(event, first) val cost = AnvilCost() @@ -274,7 +276,7 @@ class PrepareAnvilListener : Listener { private fun doMerge( event: PrepareAnvilEvent, inventory: AnvilInventory, - player: HumanEntity, + player: Player, first: ItemStack, second: ItemStack ) { val newEnchants = first.findEnchantments() @@ -327,7 +329,7 @@ class PrepareAnvilListener : Listener { // return true if there is a valid unit repair with these ingredients private fun testUnitRepair( - event: PrepareAnvilEvent, inventory: AnvilInventory, player: HumanEntity, + event: PrepareAnvilEvent, inventory: AnvilInventory, player: Player, first: ItemStack, second: ItemStack ): Boolean { val unitRepairAmount = first.getRepair(second) ?: return false @@ -356,7 +358,7 @@ class PrepareAnvilListener : Listener { } private fun testLoreEdit( - event: PrepareAnvilEvent, inventory: AnvilInventory, player: HumanEntity, + event: PrepareAnvilEvent, inventory: AnvilInventory, player: Player, first: ItemStack, second: ItemStack ): Boolean { val type = second.type diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt index 6565166..07f3d23 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt @@ -2,9 +2,12 @@ package xyz.alexcrea.cuanvil.util import io.delilaheve.CustomAnvil import io.delilaheve.util.ConfigOptions +import io.delilaheve.util.ConfigOptions.getMonetaryMultiplier as moneyMultiplier import io.delilaheve.util.EnchantmentUtil.enchantmentName import io.delilaheve.util.ItemUtil.findEnchantments import io.delilaheve.util.ItemUtil.isEnchantedBook +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.TextColor import org.bukkit.GameMode import org.bukkit.NamespacedKey import org.bukkit.entity.HumanEntity @@ -16,7 +19,9 @@ import org.bukkit.inventory.meta.Repairable import org.bukkit.persistence.PersistentDataType 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 java.math.BigDecimal import kotlin.math.min object AnvilXpUtil { @@ -43,6 +48,7 @@ object AnvilXpUtil { this.generic = generic isAlone = true } + constructor() { isAlone = false } @@ -54,12 +60,14 @@ object AnvilXpUtil { fun setAnvilInvCost( inventory: AnvilInventory, view: InventoryView, - player: HumanEntity, + player: Player, cost: AnvilCost, ignoreRules: Boolean = false ) { - // TODO check require money or xp cost & display appropriately - setAnvilInvXp(inventory, view, player, cost.sum(), ignoreRules) + if (ConfigOptions.shouldUseMoney) + setAnvilPrice(inventory, view, player, cost) + else + setAnvilInvXp(inventory, view, player, cost.sum(), ignoreRules) } /** @@ -72,7 +80,7 @@ object AnvilXpUtil { anvilCost: Int, ignoreRules: Boolean = false ) { - + // Test repair cost limit val finalAnvilCost = if ( !ignoreRules && @@ -121,6 +129,59 @@ object AnvilXpUtil { } } + fun asMonetaryCost(cost: AnvilCost): BigDecimal { + // multiply by per use type multipliers + return BigDecimal(cost.generic) + .add(BigDecimal(cost.enchantment).multiply(moneyMultiplier("enchantment"))) + .add(BigDecimal(cost.repair).multiply(moneyMultiplier("repair"))) + .add(BigDecimal(cost.rename).multiply(moneyMultiplier("rename"))) + .add(BigDecimal(cost.lore).multiply(moneyMultiplier("lore_edit"))) + .add(BigDecimal(cost.enchantment).multiply(moneyMultiplier("enchantment"))) + .add(BigDecimal(cost.illegalPenalty).multiply(moneyMultiplier("work_penalty"))) + .add(BigDecimal(cost.workPenalty).multiply(moneyMultiplier("work_penalty"))) + .add(BigDecimal(cost.recipe).multiply(moneyMultiplier("recipe"))) + .multiply(moneyMultiplier("global")) + } + + /** + * Display monetary cost needed for the work on the anvil inventory + */ + private fun setAnvilPrice( + inventory: AnvilInventory, + view: InventoryView, + player: Player, + cost: AnvilCost, + ) { + val finalCost = asMonetaryCost(cost) + + val has = EconomyManager.economy!!.has(player, finalCost) + + val text = "Cost: " + (if(has) "§2" else "§4") + + EconomyManager.economy!!.format(finalCost) + AnvilTitleUtil.rename(view, text) + + 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 From fb27ad2e552890095aa3ad734f9d59f6d600dd21 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Thu, 28 May 2026 20:11:02 +0200 Subject: [PATCH 122/166] avoid looping on same name --- .../xyz/alexcrea/cuanvil/dialog/AnvilRenameDialogImpl.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 index 603a215..fb5fa0c 100644 --- 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 @@ -109,13 +109,16 @@ class AnvilRenameDialogImpl( private fun setName(player: HumanEntity, view: CraftAnvilView, name: String?) { val menu = (containerField.get(view) as AnvilMenu) + val isSameName = menu.itemName == name menu.itemName = name if(name == null) lastNames.remove(player.uniqueId) else lastNames[player.uniqueId] = name - CraftEventFactory.callPrepareResultEvent(menu, 2); + + if(!isSameName) + CraftEventFactory.callPrepareResultEvent(menu, 2); } private fun nameFromItem(player: HumanEntity, item: ItemStack?): String? { From 171a8cad6dc6296902dea40767aaa391ab83a445 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Thu, 28 May 2026 20:21:40 +0200 Subject: [PATCH 123/166] monetary cost require dialog rename --- defaultconfigs/1.18/config.yml | 5 +++- defaultconfigs/1.21.11/config.yml | 5 +++- defaultconfigs/1.21.9/config.yml | 5 +++- defaultconfigs/1.21/config.yml | 5 +++- .../alexcrea/cuanvil/util/AnvilTitleUtil.kt | 2 ++ .../io/delilaheve/util/ConfigOptions.kt | 28 +++++++++++++------ .../cuanvil/listener/PrepareAnvilListener.kt | 21 ++++++-------- .../xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt | 10 +++++-- src/main/resources/config.yml | 5 +++- 9 files changed, 57 insertions(+), 29 deletions(-) rename nms/{v1_21R1 => nms-paper}/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilTitleUtil.kt (86%) diff --git a/defaultconfigs/1.18/config.yml b/defaultconfigs/1.18/config.yml index 1a1663a..fb70505 100644 --- a/defaultconfigs/1.18/config.yml +++ b/defaultconfigs/1.18/config.yml @@ -433,7 +433,10 @@ lore_edit: # Allow to replace the xp cost by a monetary cost # If enabled it will not be bound to the experience level limits # -# This feature can only be enabled starting with 1.21 +# 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 +# +# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 monetary_cost: enabled: false # If using vault unlocked this allow to specify what currency should be used for anvil usage diff --git a/defaultconfigs/1.21.11/config.yml b/defaultconfigs/1.21.11/config.yml index 2daa373..b70798c 100644 --- a/defaultconfigs/1.21.11/config.yml +++ b/defaultconfigs/1.21.11/config.yml @@ -453,7 +453,10 @@ lore_edit: # Allow to replace the xp cost by a monetary cost # If enabled it will not be bound to the experience level limits # -# This feature can only be enabled starting with 1.21 +# 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 +# +# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 monetary_cost: enabled: false # If using vault unlocked this allow to specify what currency should be used for anvil usage diff --git a/defaultconfigs/1.21.9/config.yml b/defaultconfigs/1.21.9/config.yml index 8532881..31bdb1f 100644 --- a/defaultconfigs/1.21.9/config.yml +++ b/defaultconfigs/1.21.9/config.yml @@ -445,7 +445,10 @@ lore_edit: # Allow to replace the xp cost by a monetary cost # If enabled it will not be bound to the experience level limits # -# This feature can only be enabled starting with 1.21 +# 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 +# +# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 monetary_cost: enabled: false # If using vault unlocked this allow to specify what currency should be used for anvil usage diff --git a/defaultconfigs/1.21/config.yml b/defaultconfigs/1.21/config.yml index 25e47dc..1dafa54 100644 --- a/defaultconfigs/1.21/config.yml +++ b/defaultconfigs/1.21/config.yml @@ -433,7 +433,10 @@ lore_edit: # Allow to replace the xp cost by a monetary cost # If enabled it will not be bound to the experience level limits # -# This feature can only be enabled starting with 1.21 +# 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 +# +# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 monetary_cost: enabled: false # If using vault unlocked this allow to specify what currency should be used for anvil usage diff --git a/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilTitleUtil.kt b/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilTitleUtil.kt similarity index 86% rename from nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilTitleUtil.kt rename to nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilTitleUtil.kt index 805b150..cc92b78 100644 --- a/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilTitleUtil.kt +++ b/nms/nms-paper/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilTitleUtil.kt @@ -6,6 +6,8 @@ import org.bukkit.inventory.InventoryView object AnvilTitleUtil { fun rename(view: InventoryView, name: String) { + if(view.title == name) return + view.title = name } diff --git a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt index 9ee7317..b163de8 100644 --- a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt +++ b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt @@ -3,15 +3,16 @@ package io.delilaheve.util import io.delilaheve.CustomAnvil import io.delilaheve.util.EnchantmentUtil.enchantmentName import org.bukkit.NamespacedKey +import org.bukkit.entity.HumanEntity 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.update.UpdateUtils import xyz.alexcrea.cuanvil.update.Version import xyz.alexcrea.cuanvil.util.AnvilUseType +import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil import java.math.BigDecimal import java.util.* @@ -90,9 +91,6 @@ object ConfigOptions { const val DEBUG_LOGGING = "debug_log" const val VERBOSE_DEBUG_LOGGING = "debug_log_verbose" - // Minimum versions - val MINIMUM_MONETARY_COST_VER = Version(1, 21, 0) - // ---------------------- // Default config values // ---------------------- @@ -181,6 +179,11 @@ object ConfigOptions { // 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 // ------------- @@ -469,6 +472,13 @@ object ConfigOptions { .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 */ @@ -643,16 +653,16 @@ object ConfigOptions { } /* - * Monetary configs (only for 1.21+) + * Monetary configs (only for 1.21.6+) + * Also require dialog rename */ - val shouldUseMoney: Boolean - get() { + fun shouldUseMoney(player: HumanEntity): Boolean { return EconomyManager.economy?.initialized() == true && - UpdateUtils.currentMinecraftVersion().greaterEqual(MINIMUM_MONETARY_COST_VER) && + canUseDialogRename(player) && ConfigHolder.DEFAULT_CONFIG .config .getBoolean(SHOULD_USE_MONEY, DEFAULT_SHOULD_USE_MONEY) - } + } val usedCurrency: String get() { diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index c71e82e..fc04dcf 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -1,6 +1,7 @@ package xyz.alexcrea.cuanvil.listener import com.github.stefvanschie.inventoryframework.util.InventoryViewUtil +import com.jankominek.disenchantment.utils.AnvilCostUtils import io.delilaheve.CustomAnvil import io.delilaheve.util.ConfigOptions import io.delilaheve.util.EnchantmentUtil.combineWith @@ -19,6 +20,7 @@ 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 @@ -45,8 +47,6 @@ class PrepareAnvilListener : Listener { const val ANVIL_OUTPUT_SLOT = 2 var IS_EMPTY_TEST = false - - private const val RENAME_DIALOG_PERMISSION = "ca.rename.dialog" } /** @@ -121,29 +121,26 @@ class PrepareAnvilListener : Listener { // Test for lore edit if (testLoreEdit(event, inventory, player, first, second)) return - CustomAnvil.log("no anvil fuse type found") event.result = null } + private fun setNoResult(event: PrepareAnvilEvent, view: InventoryView) { + event.result = null + AnvilXpUtil.onNoResult(view) + } + private fun tryRenameDialog( player: HumanEntity, event: PrepareAnvilEvent ) { - if(!canUseRenameDialog(player)) return + if(!ConfigOptions.canUseDialogRename(player)) return AnvilRenameDialogUtil.anvilRenameDialog.tryShowDialog(player, event) } - private fun canUseRenameDialog(player: HumanEntity): Boolean { - if(!ConfigOptions.doRenameDialog || !AnvilRenameDialogUtil.anvilRenameDialog.canSendDialog()) return false - if(ConfigOptions.doRenameDialogUsePermission && !player.hasPermission(RENAME_DIALOG_PERMISSION)) return false - - return true - } - private fun processDialogPCD(it: ItemMeta, player: HumanEntity) { - val keepDialog = canUseRenameDialog(player) && ConfigOptions.shouldKeepRenameText + val keepDialog = ConfigOptions.canUseDialogRename(player) && ConfigOptions.shouldKeepRenameText val pdc = it.persistentDataContainer if(!keepDialog) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt index 07f3d23..c8829f3 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt @@ -6,8 +6,6 @@ import io.delilaheve.util.ConfigOptions.getMonetaryMultiplier as moneyMultiplier import io.delilaheve.util.EnchantmentUtil.enchantmentName import io.delilaheve.util.ItemUtil.findEnchantments import io.delilaheve.util.ItemUtil.isEnchantedBook -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.TextColor import org.bukkit.GameMode import org.bukkit.NamespacedKey import org.bukkit.entity.HumanEntity @@ -21,6 +19,7 @@ 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.dialog.AnvilRenameDialogUtil import java.math.BigDecimal import kotlin.math.min @@ -64,7 +63,7 @@ object AnvilXpUtil { cost: AnvilCost, ignoreRules: Boolean = false ) { - if (ConfigOptions.shouldUseMoney) + if (ConfigOptions.shouldUseMoney(player)) setAnvilPrice(inventory, view, player, cost) else setAnvilInvXp(inventory, view, player, cost.sum(), ignoreRules) @@ -228,6 +227,11 @@ object AnvilXpUtil { return resultSum } + fun onNoResult(player: HumanEntity, view: InventoryView) { + if (ConfigOptions.shouldUseMoney(player)) + AnvilTitleUtil.rename(view, "") + } + private fun exclusivePenaltyKey(useType: AnvilUseType): NamespacedKey { return NamespacedKey(CustomAnvil.instance, "${EXCLUSIVE_PENALTY_PREFIX}_${useType.typeName}") } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 5ba61ec..27c0248 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -435,7 +435,10 @@ lore_edit: # Allow to replace the xp cost by a monetary cost # If enabled it will not be bound to the experience level limits # -# This feature can only be enabled starting with 1.21 +# 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 +# +# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 monetary_cost: enabled: false # If using vault unlocked this allow to specify what currency should be used for anvil usage From 7aac325c709b3cd73d46cac94f5873cbacdd86e6 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Fri, 29 May 2026 00:39:12 +0200 Subject: [PATCH 124/166] hell --- .../cuanvil/dialog/AnvilRenameDialog.kt | 2 + .../cuanvil/dialog/AnvilRenameDialogImpl.kt | 127 +++++++++++------- .../alexcrea/cuanvil/util/AnvilTitleUtil.kt | 37 ++++- .../cuanvil/listener/PrepareAnvilListener.kt | 6 +- .../xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt | 10 +- .../util/dialog/AnvilRenameDialogUtil.kt | 4 + 6 files changed, 130 insertions(+), 56 deletions(-) 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 index 62e1cf0..df6e883 100644 --- 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 @@ -18,4 +18,6 @@ interface AnvilRenameDialog { fun currentText(player: HumanEntity): String? + fun isOpenFor(player: HumanEntity): Boolean + } \ No newline at end of file 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 index fb5fa0c..c6973f1 100644 --- 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 @@ -32,7 +32,7 @@ class AnvilRenameDialogImpl( val keepUserPreviousDialog: Supplier, val maxLength: Supplier, val plugin: Plugin, -): AnvilRenameDialog { +) : AnvilRenameDialog { companion object { private const val RENAME_TEXT_KEY = "rename" @@ -43,16 +43,24 @@ class AnvilRenameDialogImpl( // 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_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")) } 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 { @@ -63,75 +71,90 @@ class AnvilRenameDialogImpl( return true } - fun makeDialog(initial: String?, callback: Consumer): Dialog { + 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) + .canCloseWithEscape(false) .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(), + .inputs( + listOf( + DialogInput.text(RENAME_TEXT_KEY, Component.text("Rename text")) + .maxLength(maxLength) + .initial(initialFinal ?: "") + .labelVisible(false) + .width(MAX_WIDTH) + .build(), ), ) - baseBuilder.body(listOf( + 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, _ -> - val text = response.getText(RENAME_TEXT_KEY)!! - callback.accept(text) - }, ClickCallback.Options.builder().build())) - .build(), - ActionButton.builder(USER_FACING_CANCEL) - .build(), - )) + 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: CraftAnvilView, result: String?) { val defaultName = PLAIN_TEXT_SERIALIZER.serializeOrNull(view.getItem(0)?.effectiveName()) - if(defaultName == result) { - setName(player, view, "") - if(defaultName != null) lastNames[player.uniqueId] = defaultName - } - else setName(player, view, result) + if (defaultName == result) { + setName(player, view, "", null) + if (defaultName != null) lastNames[player.uniqueId] = defaultName + } else setName(player, view, lastNames[player.uniqueId], result) } - private fun setName(player: HumanEntity, view: CraftAnvilView, name: String?) { + private fun setName(player: HumanEntity, view: CraftAnvilView, name: String?, rename: String?) { val menu = (containerField.get(view) as AnvilMenu) val isSameName = menu.itemName == name menu.itemName = name - if(name == null) + if (name == null) lastNames.remove(player.uniqueId) else lastNames[player.uniqueId] = name - if(!isSameName) + 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()) + if (item?.hasItemMeta() != true || !item.itemMeta.hasCustomName()) return PLAIN_TEXT_SERIALIZER.serializeOrNull(item?.effectiveName()) - if(keepUserPreviousDialog.get() && item.hasItemMeta()) { + if (keepUserPreviousDialog.get() && item.hasItemMeta()) { val lastName = item.itemMeta.persistentDataContainer.get( AnvilRenameDialog.PCD_KEEP_RENAME_TEXT_KEY, - PersistentDataType.STRING) + PersistentDataType.STRING + ) - if(lastName != null) return lastName + if (lastName != null) return lastName } return fromFormated.apply(player, item.effectiveName()) @@ -139,33 +162,37 @@ class AnvilRenameDialogImpl( private fun tryShowDialogScheduled(player: HumanEntity, event: PrepareAnvilEvent) { val view = event.view - if(view !is CraftAnvilView) return + 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) - if(lastLeftItem.getOrDefault(player.uniqueId, null) != leftItemStr) { - if(leftItemStr == null) + 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, nameFromItem(player, leftItem)) + setName(player, view, renameText, nameFromItem(player, leftItem)) return } - if(lastName == renameText) + if (lastName == renameText) return - if(renameText?.isBlank() == true) { - setName(player, view, lastNames[player.uniqueId]) + if (renameText?.isBlank() == true) { + setName(player, view, lastName, lastRename) return } - val dialog = makeDialog(lastName) + 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 @@ -181,7 +208,7 @@ class AnvilRenameDialogImpl( {}, 2 ) - if(task == null) return + if (task == null) return runTaskMap[player.uniqueId] = task } @@ -196,4 +223,8 @@ class AnvilRenameDialogImpl( 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 index cc92b78..e40e7ed 100644 --- 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 @@ -1,14 +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 -// TODO yet another cleanup to do on legacy removal branch object AnvilTitleUtil { - fun rename(view: InventoryView, name: String) { - if(view.title == name) return + 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/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index fc04dcf..5ef3e27 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -59,6 +59,8 @@ class PrepareAnvilListener : Listener { if(player !is Player) return val inventory = event.inventory + 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 @@ -87,8 +89,6 @@ class PrepareAnvilListener : Listener { return } - tryRenameDialog(player, event) - // Test if the event should bypass custom anvil. if (DependencyManager.tryEventPreAnvilBypass(event, player)) { // even if we got bypassed we still want to set price @@ -127,7 +127,7 @@ class PrepareAnvilListener : Listener { private fun setNoResult(event: PrepareAnvilEvent, view: InventoryView) { event.result = null - AnvilXpUtil.onNoResult(view) + // TODO AnvilXpUtil.onNoResult(view) } private fun tryRenameDialog( diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt index c8829f3..2d7bf0c 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt @@ -157,7 +157,10 @@ object AnvilXpUtil { val text = "Cost: " + (if(has) "§2" else "§4") + EconomyManager.economy!!.format(finalCost) - AnvilTitleUtil.rename(view, text) + AnvilTitleUtil.rename(view, text, + player, + AnvilRenameDialogUtil.anvilRenameDialog, + CustomAnvil.instance) clearAnvilXpCost(inventory, view, player) } @@ -229,7 +232,10 @@ object AnvilXpUtil { fun onNoResult(player: HumanEntity, view: InventoryView) { if (ConfigOptions.shouldUseMoney(player)) - AnvilTitleUtil.rename(view, "") + AnvilTitleUtil.rename(view, "", + player, + AnvilRenameDialogUtil.anvilRenameDialog, + CustomAnvil.instance) } private fun exclusivePenaltyKey(useType: AnvilUseType): NamespacedKey { diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/dialog/AnvilRenameDialogUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/dialog/AnvilRenameDialogUtil.kt index dbff77b..a83d8b0 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/dialog/AnvilRenameDialogUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/dialog/AnvilRenameDialogUtil.kt @@ -47,5 +47,9 @@ object AnvilRenameDialogUtil { return null } + override fun isOpenFor(player: HumanEntity): Boolean { + return false + } + } } \ No newline at end of file From 3992ce1662012a0c4ce4180dd6f51e22458b9f0b Mon Sep 17 00:00:00 2001 From: alexcrea Date: Fri, 29 May 2026 02:48:16 +0200 Subject: [PATCH 125/166] no price on no result --- .../cuanvil/listener/PrepareAnvilListener.kt | 53 +++++++++++-------- .../xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt | 2 +- .../cuanvil/util/JustForEasierHotswapUtil.kt | 17 ++++++ 3 files changed, 48 insertions(+), 24 deletions(-) create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/util/JustForEasierHotswapUtil.kt diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index 5ef3e27..ec5cac2 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -54,25 +54,26 @@ class PrepareAnvilListener : Listener { */ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) fun anvilCombineCheck(event: PrepareAnvilEvent) { - // Should find player - val player = InventoryViewUtil.getInstance().getPlayer(event.view) - if(player !is Player) return + val view = event.view val inventory = event.inventory + val player = JustForEasierHotswapUtil.getPlayerFromView(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, event.view, player, AnvilCost(event.inventory.repairCost)) + AnvilXpUtil.setAnvilInvCost(inventory, view, player, AnvilCost(event.inventory.repairCost)) return } - val first = inventory.getItem(ANVIL_INPUT_LEFT) ?: return + val first = inventory.getItem(ANVIL_INPUT_LEFT) val second = inventory.getItem(ANVIL_INPUT_RIGHT) if(IS_EMPTY_TEST) { - event.result = null + setNoResult(event, player, view) IS_EMPTY_TEST = false return } @@ -85,33 +86,40 @@ class PrepareAnvilListener : Listener { if (isImmutable(first) || isImmutable(second)) { CustomAnvil.verboseLog("Skipping anvil process as one of the two item is immutable") - event.result = null + setNoResult(event, player, view) 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, event.view, player, AnvilCost(event.inventory.repairCost)) + AnvilXpUtil.setAnvilInvCost(inventory, view, player, AnvilCost(event.inventory.repairCost)) return } if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return + if(first == null) { + setNoResult(event, player, view) + return + } + // Test custom recipe if (testCustomRecipe(event, inventory, player, first, second)) return // Test rename lonely item - val isAir = second.isAir - CustomAnvil.verboseLog("checking air in main logic: $isAir") - if (isAir) { - doRenaming(event, inventory, player, first) + val shouldTryRename = second.isAir + CustomAnvil.verboseLog("checking air in main logic: $shouldTryRename") + if (shouldTryRename) { + if(!doRenaming(event, inventory, player, first)) + setNoResult(event, player, view) return } // Test for merge if (first.canMergeWith(second!!)) { - doMerge(event, inventory, player, first, second) + if(!doMerge(event, inventory, player, first, second)) + setNoResult(event, player, view) return } @@ -121,13 +129,12 @@ class PrepareAnvilListener : Listener { // Test for lore edit if (testLoreEdit(event, inventory, player, first, second)) return - event.result = null - + setNoResult(event, player, view) } - private fun setNoResult(event: PrepareAnvilEvent, view: InventoryView) { + private fun setNoResult(event: PrepareAnvilEvent, player: Player, view: InventoryView) { event.result = null - // TODO AnvilXpUtil.onNoResult(view) + AnvilXpUtil.onNoResult(player, view) } private fun tryRenameDialog( @@ -209,7 +216,7 @@ class PrepareAnvilListener : Listener { private fun doRenaming( event: PrepareAnvilEvent, inventory: AnvilInventory, player: Player, first: ItemStack - ) { + ): Boolean { val resultItem = DependencyManager.cloneItem(event, first) val cost = AnvilCost() cost.rename = handleRename(resultItem, inventory, player) @@ -217,14 +224,14 @@ class PrepareAnvilListener : Listener { // Test/stop if nothing changed. if (first == resultItem) { CustomAnvil.log("no right item, But input is same as output") - event.result = null - return + return false } cost.workPenalty = AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.RENAME_ONLY) event.result = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.RENAME_ONLY, cost) AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost) + return true } private fun handleRename(resultItem: ItemStack, inventory: AnvilInventory, player: HumanEntity): Int { @@ -275,7 +282,7 @@ class PrepareAnvilListener : Listener { event: PrepareAnvilEvent, inventory: AnvilInventory, player: Player, first: ItemStack, second: ItemStack - ) { + ): Boolean { val newEnchants = first.findEnchantments() .combineWith(second.findEnchantments(), first, player) var hasChanged = !isIdentical(first.findEnchantments(), newEnchants) @@ -299,8 +306,7 @@ class PrepareAnvilListener : Listener { // Test/stop if nothing changed. if (!hasChanged) { CustomAnvil.log("Mergeable with second, But input is same as output") - event.result = null - return + return false } // As calculatePenalty edit result, we need to calculate penalty after checking equality cost.workPenalty = AnvilXpUtil.calculatePenalty(first, second, resultItem, AnvilUseType.MERGE) @@ -310,6 +316,7 @@ class PrepareAnvilListener : Listener { // Finally, we set result event.result = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.MERGE, cost) AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost) + return true } private fun isIdentical( diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt index 2d7bf0c..a581ed3 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt @@ -232,7 +232,7 @@ object AnvilXpUtil { fun onNoResult(player: HumanEntity, view: InventoryView) { if (ConfigOptions.shouldUseMoney(player)) - AnvilTitleUtil.rename(view, "", + AnvilTitleUtil.rename(view, "Repair & Name", player, AnvilRenameDialogUtil.anvilRenameDialog, CustomAnvil.instance) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/JustForEasierHotswapUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/JustForEasierHotswapUtil.kt new file mode 100644 index 0000000..ea0f7ec --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/JustForEasierHotswapUtil.kt @@ -0,0 +1,17 @@ +package xyz.alexcrea.cuanvil.util + +import com.github.stefvanschie.inventoryframework.util.InventoryViewUtil +import org.bukkit.entity.HumanEntity +import org.bukkit.inventory.InventoryView + +// Hotswap to not relocate +// So I just put small thing calling relocating method here to enable to hotswap more class +// Especially for PrepareAnvilListener +// Will be able to replace that on legacy removal so really temporary +object JustForEasierHotswapUtil { + + fun getPlayerFromView(view: InventoryView): HumanEntity { + return InventoryViewUtil.getInstance().getPlayer(view) + } + +} \ No newline at end of file From 2d31a7f5a8dac3a618659fec3896bc1ed07813de Mon Sep 17 00:00:00 2001 From: alexcrea Date: Fri, 29 May 2026 03:18:59 +0200 Subject: [PATCH 126/166] seems to work better --- .../cuanvil/dialog/AnvilRenameDialogImpl.kt | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) 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 index c6973f1..e210daa 100644 --- 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 @@ -18,6 +18,7 @@ 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 @@ -49,6 +50,10 @@ class AnvilRenameDialogImpl( ) 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() @@ -76,7 +81,7 @@ class AnvilRenameDialogImpl( val initialFinal = initial?.take(maxLength) val baseBuilder = DialogBase.builder(USER_FACING_RENAME_TITLE) - .canCloseWithEscape(false) + .canCloseWithEscape(true) .afterAction(DialogBase.DialogAfterAction.CLOSE) .inputs( listOf( @@ -116,18 +121,18 @@ class AnvilRenameDialogImpl( } } - private fun setResult(player: HumanEntity, view: CraftAnvilView, result: String?) { - val defaultName = PLAIN_TEXT_SERIALIZER.serializeOrNull(view.getItem(0)?.effectiveName()) + 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, lastNames[player.uniqueId], result) + } else setName(player, view, result, result) } - private fun setName(player: HumanEntity, view: CraftAnvilView, name: String?, rename: String?) { + 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 = name + menu.itemName = rename if (name == null) lastNames.remove(player.uniqueId) @@ -180,10 +185,10 @@ class AnvilRenameDialogImpl( return } - if (lastName == renameText) + if (lastName == renameText || lastRename == renameText) return - if (renameText?.isBlank() == true) { + if (renameText?.isBlank() == true || renameText == itemDefaultName(leftItem)) { setName(player, view, lastName, lastRename) return } @@ -215,6 +220,7 @@ class AnvilRenameDialogImpl( override fun closeInventory(player: HumanEntity) { lastNames.remove(player.uniqueId) + lastRenames.remove(player.uniqueId) lastLeftItem.remove(player.uniqueId) runTaskMap.remove(player.uniqueId)?.cancel() } From bf8144ad06146f0983f58771ce165ce3f1ab55a4 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sat, 30 May 2026 03:53:31 +0200 Subject: [PATCH 127/166] result work for unity repair and custom craft --- .../listener/CATreatAnvilResultEvent.java | 4 +- .../cuanvil/listener/AnvilResultListener.kt | 77 +++++++++++++------ .../xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt | 64 ++++++++------- 3 files changed, 90 insertions(+), 55 deletions(-) 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 index 4d68aca..7bc8fa3 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResultEvent.java +++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResultEvent.java @@ -109,7 +109,7 @@ public class CATreatAnvilResultEvent extends Event { */ @Deprecated(forRemoval = true, since = "1.17.0") public int getLevelCost() { - return cost.sum(); + return cost.asXpCost(); } /** @@ -131,7 +131,7 @@ public class CATreatAnvilResultEvent extends Event { */ @Deprecated(forRemoval = true, since = "1.17.0") public void setLevelCost(int levelCost) { - cost.setGeneric(levelCost - cost.getGeneric() - cost.sum()); + cost.setGeneric(levelCost - cost.getGeneric() - cost.asXpCost()); } /** diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt index 1886589..77bca4a 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -17,6 +17,7 @@ import org.bukkit.inventory.InventoryView import org.bukkit.inventory.ItemStack import org.bukkit.inventory.meta.BookMeta 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 @@ -32,7 +33,6 @@ import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil import xyz.alexcrea.cuanvil.util.config.LoreEditType import java.util.* -import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference import kotlin.math.min @@ -89,6 +89,7 @@ class AnvilResultListener : Listener { // Rename if (rightItem == null) { + // BRUH event.result = Event.Result.ALLOW return } @@ -246,14 +247,13 @@ class AnvilResultListener : Listener { rightItem: ItemStack?, rightRemoveCount: Int, output: ItemStack, - repairCost: Int, + cost: AnvilCost, ): Boolean { // To avoid vanilla, we cancel the event event.result = Event.Result.DENY event.isCancelled = true - // Assumed if player do not have enough xp then it returned MIN_VALUE - if (repairCost == Int.MIN_VALUE) return false + if (!cost.valid) return false // Where should we get the item val slotDestination = getActionSlot(event, player) @@ -261,6 +261,13 @@ class AnvilResultListener : Listener { // If not creative middle click... if (event.click != ClickType.MIDDLE) { + if(cost.isMonetary) { + val result = EconomyManager.economy!!.remove(player, cost.asMonetaryCost()) + if(!result) return false + } else { + player.level -= cost.asXpCost() + } + // We remove what should be removed if (leftItem != null) leftItem.amount -= leftRemoveCount inventory.setItem(ANVIL_INPUT_LEFT, leftItem) @@ -269,7 +276,7 @@ class AnvilResultListener : Listener { inventory.setItem(ANVIL_INPUT_RIGHT, rightItem) inventory.setItem(ANVIL_OUTPUT_SLOT, null) - player.level -= repairCost + } // Finally, we add the item to the player @@ -313,55 +320,75 @@ class AnvilResultListener : Listener { inventory: AnvilInventory, player: Player, leftItem: ItemStack, output: ItemStack, resultCopy: ItemStack, resultAmount: Int - ): Int { - if (player.gameMode == GameMode.CREATIVE) return 0 + ): AnvilCost { + if (player.gameMode == GameMode.CREATIVE) return AnvilCost(0) - var repairCost = 0 + val cost = AnvilCost() // Get repairCost leftItem.itemMeta?.let { leftMeta -> val leftName = leftMeta.displayName output.itemMeta?.let { // Rename cost if (!leftName.contentEquals(it.displayName)) { - repairCost += ConfigOptions.itemRenameCost + cost.rename += ConfigOptions.itemRenameCost // Color cost if (it.displayName.contains('§')) { - repairCost += ConfigOptions.useOfColorCost + cost.rename += ConfigOptions.useOfColorCost } } } } - repairCost += AnvilXpUtil.calculatePenalty(leftItem, null, resultCopy, AnvilUseType.UNIT_REPAIR) - repairCost += resultAmount * ConfigOptions.unitRepairCost + cost.workPenalty = AnvilXpUtil.calculatePenalty(leftItem, null, resultCopy, AnvilUseType.UNIT_REPAIR) + cost.repair = resultAmount * ConfigOptions.unitRepairCost + + var sum = cost.repair if ( !ConfigOptions.doRemoveCostLimit && ConfigOptions.doCapCost ) { - repairCost = min(repairCost, ConfigOptions.maxAnvilCost) + val final = min(sum, ConfigOptions.maxAnvilCost) + cost.generic += (final - sum) + + sum = final } - if ((inventory.maximumRepairCost <= repairCost) - || (player.level < repairCost) - ) return Int.MIN_VALUE + 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 + } - return repairCost + return cost } private fun getFromLoreEditXpCost( cost: AnvilCost, player: Player, inventory: AnvilInventory, - ): Int { - if (GameMode.CREATIVE == player.gameMode) return 0 + ): AnvilCost { + if (GameMode.CREATIVE == player.gameMode) return AnvilCost(0) - val repairCost = cost.sum() - return if ((inventory.maximumRepairCost <= repairCost) - || (player.level < repairCost) - ) Int.MIN_VALUE - else repairCost + if (ConfigOptions.shouldUseMoney(player)) { + cost.isMonetary = true + if (!EconomyManager.economy!!.has(player, cost.asMonetaryCost())) + cost.valid = false + } else { + val repairCost = cost.asXpCost() + + if ((inventory.maximumRepairCost <= repairCost) + || (player.level < repairCost) + ) + cost.valid = false + } + + return cost } private fun handleBookLoreEdit( @@ -414,7 +441,7 @@ class AnvilResultListener : Listener { val bookPage = StringBuilder() lore.forEach { if (bookPage.isNotEmpty()) bookPage.append('\n') - if(it == null) return@forEach + if (it == null) return@forEach bookPage.append(MiniMessageUtil.plain_text_mm.serialize(it)) } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt index a581ed3..cedc07c 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt @@ -29,6 +29,8 @@ object AnvilXpUtil { 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 @@ -39,10 +41,6 @@ object AnvilXpUtil { var workPenalty = 0 var recipe = 0 - fun sum(): Int { - return generic + enchantment + repair + rename + lore + illegalPenalty + workPenalty + recipe - } - constructor(generic: Int) { this.generic = generic isAlone = true @@ -51,6 +49,24 @@ object AnvilXpUtil { constructor() { isAlone = false } + + fun asXpCost(): Int { + return generic + enchantment + repair + rename + lore + illegalPenalty + workPenalty + recipe + } + + 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")) + } } /** @@ -63,10 +79,11 @@ object AnvilXpUtil { cost: AnvilCost, ignoreRules: Boolean = false ) { - if (ConfigOptions.shouldUseMoney(player)) + if (ConfigOptions.shouldUseMoney(player)) { + cost.isMonetary = true setAnvilPrice(inventory, view, player, cost) - else - setAnvilInvXp(inventory, view, player, cost.sum(), ignoreRules) + } else + setAnvilInvXp(inventory, view, player, cost.asXpCost(), ignoreRules) } /** @@ -128,20 +145,6 @@ object AnvilXpUtil { } } - fun asMonetaryCost(cost: AnvilCost): BigDecimal { - // multiply by per use type multipliers - return BigDecimal(cost.generic) - .add(BigDecimal(cost.enchantment).multiply(moneyMultiplier("enchantment"))) - .add(BigDecimal(cost.repair).multiply(moneyMultiplier("repair"))) - .add(BigDecimal(cost.rename).multiply(moneyMultiplier("rename"))) - .add(BigDecimal(cost.lore).multiply(moneyMultiplier("lore_edit"))) - .add(BigDecimal(cost.enchantment).multiply(moneyMultiplier("enchantment"))) - .add(BigDecimal(cost.illegalPenalty).multiply(moneyMultiplier("work_penalty"))) - .add(BigDecimal(cost.workPenalty).multiply(moneyMultiplier("work_penalty"))) - .add(BigDecimal(cost.recipe).multiply(moneyMultiplier("recipe"))) - .multiply(moneyMultiplier("global")) - } - /** * Display monetary cost needed for the work on the anvil inventory */ @@ -151,16 +154,19 @@ object AnvilXpUtil { player: Player, cost: AnvilCost, ) { - val finalCost = asMonetaryCost(cost) + val finalCost = cost.asMonetaryCost() - val has = EconomyManager.economy!!.has(player, finalCost) + val has = player.gameMode == GameMode.CREATIVE || + EconomyManager.economy!!.has(player, finalCost) - val text = "Cost: " + (if(has) "§2" else "§4") + + val text = "Cost: " + (if (has) "§2" else "§4") + EconomyManager.economy!!.format(finalCost) - AnvilTitleUtil.rename(view, text, + AnvilTitleUtil.rename( + view, text, player, AnvilRenameDialogUtil.anvilRenameDialog, - CustomAnvil.instance) + CustomAnvil.instance + ) clearAnvilXpCost(inventory, view, player) } @@ -232,10 +238,12 @@ object AnvilXpUtil { fun onNoResult(player: HumanEntity, view: InventoryView) { if (ConfigOptions.shouldUseMoney(player)) - AnvilTitleUtil.rename(view, "Repair & Name", + AnvilTitleUtil.rename( + view, "Repair & Name", player, AnvilRenameDialogUtil.anvilRenameDialog, - CustomAnvil.instance) + CustomAnvil.instance + ) } private fun exclusivePenaltyKey(useType: AnvilUseType): NamespacedKey { From df92b4bf913532ad5782144bc780c03cfe54c5fd Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 1 Jun 2026 13:48:59 +0200 Subject: [PATCH 128/166] update faststat to 0.24.0 --- build.gradle.kts | 2 +- .../xyz/alexcrea/cuanvil/util/MetricsUtil.kt | 24 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 1ff46b1..db82f1c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -55,7 +55,7 @@ dependencies { compileOnly("org.spigotmc:spigot-api:1.18-R0.1-SNAPSHOT") // fast stats - implementation("dev.faststats.metrics:bukkit:0.22.1") + implementation("dev.faststats.metrics:bukkit:0.24.0") // minimessage implementation("net.kyori:adventure-text-minimessage:4.25.0") diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt index 80a84c2..81ac870 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt @@ -1,8 +1,8 @@ package xyz.alexcrea.cuanvil.util -import dev.faststats.bukkit.BukkitMetrics -import dev.faststats.core.ErrorTracker -import dev.faststats.core.data.Metric +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 @@ -14,7 +14,7 @@ object MetricsUtil { private const val FASTSTATS_TOKEN = "fc282b048adcc71a77bc00ace49e8a81" private var ERROR_TRACKER: ErrorTracker? = null - private var FAST_STATS_METRICS: BukkitMetrics? = null + private var FAST_STATS_METRICS: BukkitContext? = null fun loadMetrics(plugin: CustomAnvil) { val config = ConfigHolder.DEFAULT_CONFIG.config @@ -47,13 +47,15 @@ object MetricsUtil { if(reportErrors) ERROR_TRACKER = ErrorTracker.contextAware() - FAST_STATS_METRICS = BukkitMetrics.factory() - .addMetric(Metric.string("nms_type") { nmsType }) - .addMetric(Metric.bool("replace_too_expensive") { ConfigOptions.doReplaceTooExpensive }) - .addMetric(Metric.bool("using_alpha") { isAlpha }) - .errorTracker(ERROR_TRACKER) - .token(FASTSTATS_TOKEN) - .create(plugin) + 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() } From 7a705f3bfcef7bc648bd56a53aa40060110c3799 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 2 Jun 2026 13:29:26 +0200 Subject: [PATCH 129/166] move a lot of function to AnvilMergeLogic.kt --- .../listener/CATreatAnvilResultEvent.java | 8 +- .../cuanvil/config/WorkPenaltyType.java | 2 +- .../settings/WorkPenaltyTypeSettingGui.java | 2 +- .../cuanvil/update/plugin/PUpdate_1_8_0.java | 2 +- .../io/delilaheve/util/ConfigOptions.kt | 3 +- .../alexcrea/cuanvil/anvil/AnvilMergeLogic.kt | 271 ++++++++++++++++ .../cuanvil/{util => anvil}/AnvilUseType.kt | 26 +- .../cuanvil/dependency/DependencyManager.kt | 15 +- .../plugins/DisenchantmentDependency.kt | 4 +- .../dependency/plugins/HavenBagsDependency.kt | 4 +- .../cuanvil/listener/AnvilResultListener.kt | 8 +- .../cuanvil/listener/PrepareAnvilListener.kt | 295 +++--------------- .../cuanvil/recipe/AnvilCustomRecipe.kt | 4 +- .../util/{ => anvil}/AnvilColorUtil.kt | 4 +- .../util/{ => anvil}/AnvilLoreEditUtil.kt | 18 +- .../util/{ => anvil}/AnvilUseTypeUtil.kt | 2 +- .../cuanvil/util/{ => anvil}/AnvilXpUtil.kt | 6 +- .../cuanvil/util/config/LoreEditType.kt | 2 +- .../util/dialog/AnvilRenameDialogUtil.kt | 2 +- 19 files changed, 373 insertions(+), 305 deletions(-) create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt rename src/main/kotlin/xyz/alexcrea/cuanvil/{util => anvil}/AnvilUseType.kt (66%) rename src/main/kotlin/xyz/alexcrea/cuanvil/util/{ => anvil}/AnvilColorUtil.kt (99%) rename src/main/kotlin/xyz/alexcrea/cuanvil/util/{ => anvil}/AnvilLoreEditUtil.kt (96%) rename src/main/kotlin/xyz/alexcrea/cuanvil/util/{ => anvil}/AnvilUseTypeUtil.kt (96%) rename src/main/kotlin/xyz/alexcrea/cuanvil/util/{ => anvil}/AnvilXpUtil.kt (98%) 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 index 7bc8fa3..ddbaf23 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResultEvent.java +++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResultEvent.java @@ -6,8 +6,8 @@ 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.util.AnvilUseType; -import xyz.alexcrea.cuanvil.util.AnvilXpUtil.AnvilCost; +import xyz.alexcrea.cuanvil.anvil.AnvilUseType; +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil.AnvilCost; /** * Called after custom anvil processed the click on the result on the anvil inventory. @@ -18,8 +18,12 @@ import xyz.alexcrea.cuanvil.util.AnvilXpUtil.AnvilCost; * and {@link CAEarlyPreAnvilBypassEvent} for your use case *

    * A null result will cancel this pre anvil event + * + * @deprecated Prepare anvil Event should not be provided as it can be called on result and therefor not have prepare anvil event + * TODO a replacement is necessary but not yet made */ @SuppressWarnings("unused") +@Deprecated(forRemoval = true, since = "1.17.0") public class CATreatAnvilResultEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); diff --git a/src/main/java/xyz/alexcrea/cuanvil/config/WorkPenaltyType.java b/src/main/java/xyz/alexcrea/cuanvil/config/WorkPenaltyType.java index 75b0861..d374999 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/config/WorkPenaltyType.java +++ b/src/main/java/xyz/alexcrea/cuanvil/config/WorkPenaltyType.java @@ -2,7 +2,7 @@ package xyz.alexcrea.cuanvil.config; import com.google.common.collect.ImmutableMap; import org.jetbrains.annotations.Nullable; -import xyz.alexcrea.cuanvil.util.AnvilUseType; +import xyz.alexcrea.cuanvil.anvil.AnvilUseType; import java.util.EnumMap; 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 index 888aa25..4345aa1 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/WorkPenaltyTypeSettingGui.java +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/WorkPenaltyTypeSettingGui.java @@ -11,11 +11,11 @@ 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 xyz.alexcrea.cuanvil.util.AnvilUseType; import java.util.ArrayList; import java.util.EnumMap; 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 index 7685f92..81ce1aa 100644 --- 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 @@ -2,10 +2,10 @@ 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 xyz.alexcrea.cuanvil.util.AnvilUseType; import javax.annotation.Nonnull; import java.util.EnumMap; diff --git a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt index b163de8..9dc85f9 100644 --- a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt +++ b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt @@ -4,14 +4,13 @@ import io.delilaheve.CustomAnvil import io.delilaheve.util.EnchantmentUtil.enchantmentName 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.update.Version -import xyz.alexcrea.cuanvil.util.AnvilUseType import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil import java.math.BigDecimal import java.util.* 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..9840cb8 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt @@ -0,0 +1,271 @@ +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.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.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.anvil.AnvilXpUtil.AnvilCost +import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil + +object AnvilMergeUtil { + + 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 + } + } + + fun doRenaming(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(resultItem, AnvilUseType.RENAME_ONLY, cost) + + return AnvilResult(result, cost) + } + + private fun processDialogPCD(it: ItemMeta, player: HumanEntity) { + val keepDialog = ConfigOptions.canUseDialogRename(player) && ConfigOptions.shouldKeepRenameText + + val pdc = it.persistentDataContainer + if(!keepDialog) + pdc.remove(AnvilRenameDialog.PCD_KEEP_RENAME_TEXT_KEY) + else { + val text = AnvilRenameDialogUtil.anvilRenameDialog.currentText(player) + 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( + 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(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( + player: Player, + first: ItemStack, second: ItemStack? + ): AnvilResult { + val recipe = CustomRecipeUtil.getCustomRecipe(first, second) + CustomAnvil.verboseLog("custom recipe not null? ${recipe != null}") + if (recipe == null) return AnvilResult.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 = AnvilCost() + cost.recipe = if (recipe.removeExactLinearXp) AnvilXpUtil.calculateMinimumLevelForXp(xpCost) + else AnvilXpUtil.calculateLevelForXp(xpCost) + + val result = DependencyManager.tryTreatAnvilResult(resultItem, AnvilUseType.CUSTOM_CRAFT, cost) + return AnvilResult(result, cost, true) + } + + fun testUnitRepair( + inventory: AnvilInventory, + player: Player, + first: ItemStack, second: ItemStack + ): UnitRepairResult { + val unitRepairAmount = first.getRepair(second) ?: return UnitRepairResult.EMPTY + + 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(resultItem, AnvilUseType.UNIT_REPAIR, cost) + return UnitRepairResult(result, cost, repairAmount) + } + + fun testLoreEdit( + player: Player, + first: ItemStack, second: ItemStack + ): AnvilResult { + val type = second.type + var resultItem: ItemStack? = null + + val cost = AnvilCost() + if (Material.WRITABLE_BOOK == type) { + resultItem = AnvilLoreEditUtil.tryLoreEditByBook(player, first, second, cost) + } else if (Material.PAPER == type) { + resultItem = AnvilLoreEditUtil.tryLoreEditByPaper(player, first, second, cost) + } + + if (resultItem.isAir || first == resultItem) { + CustomAnvil.log("lore edit, But input is same as output") + return AnvilResult.EMPTY + } + + return AnvilResult(resultItem, cost) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilUseType.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilUseType.kt similarity index 66% rename from src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilUseType.kt rename to src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilUseType.kt index d17e908..67782f3 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilUseType.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilUseType.kt @@ -1,60 +1,60 @@ -package xyz.alexcrea.cuanvil.util +package xyz.alexcrea.cuanvil.anvil import org.bukkit.Material -import xyz.alexcrea.cuanvil.config.WorkPenaltyType.WorkPenaltyPart -import xyz.alexcrea.cuanvil.util.config.LoreEditType +import xyz.alexcrea.cuanvil.config.WorkPenaltyType +import xyz.alexcrea.cuanvil.util.anvil.AnvilUseTypeUtil enum class AnvilUseType( val typeName: String, val path: String, - val defaultPenalty: WorkPenaltyPart, + val defaultPenalty: WorkPenaltyType.WorkPenaltyPart, val displayName: String, val displayMat: Material ) { RENAME_ONLY( "rename_only", - WorkPenaltyPart(false, true), + WorkPenaltyType.WorkPenaltyPart(false, true), "Rename Only", Material.NAME_TAG ), MERGE( "merge", - WorkPenaltyPart(true, true), + WorkPenaltyType.WorkPenaltyPart(true, true), "Merge", Material.ANVIL ), UNIT_REPAIR( "unit_repair", - WorkPenaltyPart(true, true), + WorkPenaltyType.WorkPenaltyPart(true, true), "Unit Repair", Material.DIAMOND ), CUSTOM_CRAFT( "custom_craft", - WorkPenaltyPart(false, false), + WorkPenaltyType.WorkPenaltyPart(false, false), "Custom Craft", Material.CRAFTING_TABLE ), LORE_EDIT_BOOK_APPEND( "lore_edit_book_append", "lore_edit.book_and_quil.append", - WorkPenaltyPart(false, false), + WorkPenaltyType.WorkPenaltyPart(false, false), "Book Add", Material.WRITABLE_BOOK ), LORE_EDIT_BOOK_REMOVE( "lore_edit_book_remove", "lore_edit.book_and_quil.remove", - WorkPenaltyPart(false, false), + WorkPenaltyType.WorkPenaltyPart(false, false), "Book Remove", Material.WRITABLE_BOOK ), LORE_EDIT_PAPER_APPEND( "lore_edit_paper_append", "lore_edit.paper.append_line", - WorkPenaltyPart(false, false), + WorkPenaltyType.WorkPenaltyPart(false, false), "Paper Add", Material.WRITABLE_BOOK ), LORE_EDIT_PAPER_REMOVE( "lore_edit_paper_remove", "lore_edit.paper.remove_line", - WorkPenaltyPart(false, false), + WorkPenaltyType.WorkPenaltyPart(false, false), "Paper Remove", Material.WRITABLE_BOOK ), ; constructor( typeName: String, - defaultPenalty: WorkPenaltyPart, + defaultPenalty: WorkPenaltyType.WorkPenaltyPart, displayName: String, displayMat: Material ) : this( diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt index 418dbf1..4ad6c9b 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt @@ -13,6 +13,7 @@ import org.bukkit.event.inventory.PrepareAnvilEvent import org.bukkit.inventory.AnvilInventory import org.bukkit.inventory.Inventory import org.bukkit.inventory.ItemStack +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 @@ -30,9 +31,8 @@ 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.AnvilUseType -import xyz.alexcrea.cuanvil.util.AnvilXpUtil import xyz.alexcrea.cuanvil.util.MetricsUtil.trackError +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil import java.util.logging.Level object DependencyManager { @@ -234,19 +234,20 @@ object DependencyManager { // Return null if there was an issue fun tryTreatAnvilResult( - event: PrepareAnvilEvent, result: ItemStack, useType: AnvilUseType, cost: AnvilXpUtil.AnvilCost ): ItemStack? { - val treatEvent = CATreatAnvilResultEvent(event, useType, result, cost) + //TODO + /*val treatEvent = CATreatAnvilResultEvent(event, useType, result, cost) try { unsafeTryTreatAnvilResult(treatEvent) return treatEvent.result } catch (e: Exception) { logExceptionAndClear(event.view.player, event.inventory, e) return null - } + }*/ + return result } private fun unsafeTryTreatAnvilResult(event: CATreatAnvilResultEvent) { @@ -295,11 +296,11 @@ object DependencyManager { } // Clone item and use plugin specific clone if needed - fun cloneItem(event: PrepareAnvilEvent, item: ItemStack): ItemStack { + fun cloneItem(player: HumanEntity, item: ItemStack): ItemStack { try { return unsafeCloneItem(item) } catch (e: Exception) { - logException(event.view.player, e) + logException(player, e) return item.clone() } } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt index 7a7a5eb..32ca99d 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt @@ -15,9 +15,9 @@ import org.bukkit.event.inventory.PrepareAnvilEvent import org.bukkit.inventory.AnvilInventory import org.bukkit.inventory.ItemStack import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener -import xyz.alexcrea.cuanvil.util.AnvilXpUtil -import xyz.alexcrea.cuanvil.util.AnvilXpUtil.AnvilCost import xyz.alexcrea.cuanvil.util.MetricsUtil.trackError +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil.AnvilCost import java.util.logging.Level import kotlin.reflect.KClass diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt index b2e7ef4..62d9e4e 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt @@ -10,8 +10,8 @@ import valorless.havenbags.HavenBags import valorless.havenbags.features.BagSkin import valorless.havenbags.features.BagUpgrade import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener -import xyz.alexcrea.cuanvil.util.AnvilXpUtil -import xyz.alexcrea.cuanvil.util.AnvilXpUtil.AnvilCost +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil.AnvilCost class HavenBagsDependency { diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt index 77bca4a..7497e7f 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -16,6 +16,7 @@ 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.AnvilUseType import xyz.alexcrea.cuanvil.dependency.DependencyManager import xyz.alexcrea.cuanvil.dependency.economy.EconomyManager import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil.setComponentDisplayName @@ -23,13 +24,12 @@ import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener.Companion.ANVIL_INPUT_ import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener.Companion.ANVIL_INPUT_RIGHT import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener.Companion.ANVIL_OUTPUT_SLOT import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe -import xyz.alexcrea.cuanvil.util.AnvilLoreEditUtil -import xyz.alexcrea.cuanvil.util.AnvilUseType -import xyz.alexcrea.cuanvil.util.AnvilXpUtil -import xyz.alexcrea.cuanvil.util.AnvilXpUtil.AnvilCost import xyz.alexcrea.cuanvil.util.CustomRecipeUtil import xyz.alexcrea.cuanvil.util.MiniMessageUtil import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair +import xyz.alexcrea.cuanvil.util.anvil.AnvilLoreEditUtil +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil.AnvilCost import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil import xyz.alexcrea.cuanvil.util.config.LoreEditType import java.util.* diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index ec5cac2..312baba 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -1,18 +1,8 @@ package xyz.alexcrea.cuanvil.listener -import com.github.stefvanschie.inventoryframework.util.InventoryViewUtil -import com.jankominek.disenchantment.utils.AnvilCostUtils import io.delilaheve.CustomAnvil import io.delilaheve.util.ConfigOptions -import io.delilaheve.util.EnchantmentUtil.combineWith import io.delilaheve.util.ItemUtil.canMergeWith -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.event.EventHandler @@ -20,18 +10,20 @@ 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 org.bukkit.persistence.PersistentDataType +import xyz.alexcrea.cuanvil.anvil.AnvilMergeUtil.AnvilResult +import xyz.alexcrea.cuanvil.anvil.AnvilMergeUtil.doMerge +import xyz.alexcrea.cuanvil.anvil.AnvilMergeUtil.doRenaming +import xyz.alexcrea.cuanvil.anvil.AnvilMergeUtil.testCustomRecipe +import xyz.alexcrea.cuanvil.anvil.AnvilMergeUtil.testLoreEdit +import xyz.alexcrea.cuanvil.anvil.AnvilMergeUtil.testUnitRepair import xyz.alexcrea.cuanvil.dependency.DependencyManager -import xyz.alexcrea.cuanvil.dialog.AnvilRenameDialog -import xyz.alexcrea.cuanvil.enchant.CAEnchantment -import xyz.alexcrea.cuanvil.util.* -import xyz.alexcrea.cuanvil.util.AnvilXpUtil.AnvilCost +import xyz.alexcrea.cuanvil.util.JustForEasierHotswapUtil import xyz.alexcrea.cuanvil.util.MaterialUtil.isAir -import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil.AnvilCost import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil /** @@ -73,8 +65,8 @@ class PrepareAnvilListener : Listener { val second = inventory.getItem(ANVIL_INPUT_RIGHT) if(IS_EMPTY_TEST) { - setNoResult(event, player, view) IS_EMPTY_TEST = false + applyResult(event, player, AnvilResult.EMPTY) return } @@ -86,7 +78,7 @@ class PrepareAnvilListener : Listener { if (isImmutable(first) || isImmutable(second)) { CustomAnvil.verboseLog("Skipping anvil process as one of the two item is immutable") - setNoResult(event, player, view) + applyResult(event, player, AnvilResult.EMPTY) return } @@ -99,42 +91,44 @@ class PrepareAnvilListener : Listener { if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return - if(first == null) { - setNoResult(event, player, view) - return - } + val result = getResult(inventory, player, first, second) + applyResult(event, player, result) + } + + fun getResult( + inventory: AnvilInventory, + player: Player, + first: ItemStack?, second: ItemStack?) : AnvilResult + { + if(first == null) + return AnvilResult.EMPTY // Test custom recipe - if (testCustomRecipe(event, inventory, player, first, second)) return + var result = testCustomRecipe(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) { - if(!doRenaming(event, inventory, player, first)) - setNoResult(event, player, view) - return - } + if (shouldTryRename) + return doRenaming(inventory, player, first) // Test for merge - if (first.canMergeWith(second!!)) { - if(!doMerge(event, inventory, player, first, second)) - setNoResult(event, player, view) - return - } + if (first.canMergeWith(second!!)) + return doMerge(inventory, player, first, second) // Test for unit repair - if (testUnitRepair(event, inventory, player, first, second)) return + result = testUnitRepair(inventory, player, first, second) + if (!result.isEmpty()) + return result // Test for lore edit - if (testLoreEdit(event, inventory, player, first, second)) return + result = testLoreEdit(player, first, second) + if (!result.isEmpty()) + return result - setNoResult(event, player, view) - } - - private fun setNoResult(event: PrepareAnvilEvent, player: Player, view: InventoryView) { - event.result = null - AnvilXpUtil.onNoResult(player, view) + return AnvilResult.EMPTY } private fun tryRenameDialog( @@ -146,20 +140,6 @@ class PrepareAnvilListener : Listener { AnvilRenameDialogUtil.anvilRenameDialog.tryShowDialog(player, event) } - private fun processDialogPCD(it: ItemMeta, player: HumanEntity) { - val keepDialog = ConfigOptions.canUseDialogRename(player) && ConfigOptions.shouldKeepRenameText - - val pdc = it.persistentDataContainer - if(!keepDialog) - pdc.remove(AnvilRenameDialog.PCD_KEEP_RENAME_TEXT_KEY) - else { - val text = AnvilRenameDialogUtil.anvilRenameDialog.currentText(player) - 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 isImmutable(item: ItemStack?): Boolean { if (item.isAir) return false @@ -186,203 +166,14 @@ class PrepareAnvilListener : Listener { return false } - // return true if a custom recipe exist with these ingredients - private fun testCustomRecipe( - event: PrepareAnvilEvent, inventory: AnvilInventory, - player: Player, - first: ItemStack, second: ItemStack? - ): Boolean { - val recipe = CustomRecipeUtil.getCustomRecipe(first, second) - CustomAnvil.verboseLog("custom recipe not null? ${recipe != null}") - if (recipe == null) return false + private fun applyResult(event: PrepareAnvilEvent, player: Player, result: AnvilResult) { + event.result = result.item - val amount = CustomRecipeUtil.getCustomRecipeAmount(recipe, first, second) - - val resultItem: ItemStack = DependencyManager.cloneItem(event, 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 = AnvilCost() - cost.recipe = if (recipe.removeExactLinearXp) AnvilXpUtil.calculateMinimumLevelForXp(xpCost) - else AnvilXpUtil.calculateLevelForXp(xpCost) - - event.result = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.CUSTOM_CRAFT, cost) - AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost, true) - return true + if(result.item == null) { + AnvilXpUtil.onNoResult(player, event.view) + return + } + AnvilXpUtil.setAnvilInvCost(event.inventory, event.view, player, result.cost, result.ignoreXpRules) } - private fun doRenaming( - event: PrepareAnvilEvent, inventory: AnvilInventory, - player: Player, first: ItemStack - ): Boolean { - val resultItem = DependencyManager.cloneItem(event, 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 false - } - - cost.workPenalty = AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.RENAME_ONLY) - - event.result = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.RENAME_ONLY, cost) - AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost) - return true - } - - 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 - } - - private fun doMerge( - event: PrepareAnvilEvent, inventory: AnvilInventory, - player: Player, - first: ItemStack, second: ItemStack - ): Boolean { - val newEnchants = first.findEnchantments() - .combineWith(second.findEnchantments(), first, player) - var hasChanged = !isIdentical(first.findEnchantments(), newEnchants) - - val resultItem = DependencyManager.cloneItem(event, 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 false - } - // 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) - - // Finally, we set result - event.result = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.MERGE, cost) - AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost) - return true - } - - 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 there is a valid unit repair with these ingredients - private fun testUnitRepair( - event: PrepareAnvilEvent, inventory: AnvilInventory, player: Player, - first: ItemStack, second: ItemStack - ): Boolean { - val unitRepairAmount = first.getRepair(second) ?: return false - - val resultItem = DependencyManager.cloneItem(event, 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") - event.result = null - return true - } - - event.result = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.UNIT_REPAIR, cost) - AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost) - return true - } - - private fun testLoreEdit( - event: PrepareAnvilEvent, inventory: AnvilInventory, player: Player, - first: ItemStack, second: ItemStack - ): Boolean { - val type = second.type - var result: ItemStack? = null - - val cost = AnvilCost() - if (Material.WRITABLE_BOOK == type) { - result = AnvilLoreEditUtil.tryLoreEditByBook(player, first, second, cost) - } else if (Material.PAPER == type) { - result = AnvilLoreEditUtil.tryLoreEditByPaper(player, first, second, cost) - } - - if (result.isAir || first == result) { - CustomAnvil.log("lore edit, But input is same as output") - event.result = null - return false - } - - event.result = result - AnvilXpUtil.setAnvilInvCost(inventory, event.view, player, cost) - return true - } } \ 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 index 57eb124..fd079c3 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/AnvilCustomRecipe.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/recipe/AnvilCustomRecipe.kt @@ -3,11 +3,11 @@ 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.AnvilUseType -import xyz.alexcrea.cuanvil.util.AnvilXpUtil import xyz.alexcrea.cuanvil.util.MaterialUtil.isAir +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil class AnvilCustomRecipe( val name: String, diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilColorUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilColorUtil.kt similarity index 99% rename from src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilColorUtil.kt rename to src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilColorUtil.kt index 0492d32..9564b2e 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilColorUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilColorUtil.kt @@ -1,11 +1,11 @@ -package xyz.alexcrea.cuanvil.util +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 -import kotlin.text.indexOf object AnvilColorUtil { private val HEX_PATTERN: Pattern = Pattern.compile("#[A-Fa-f0-9]{6}") // pattern to find hexadecimal string diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilLoreEditUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilLoreEditUtil.kt similarity index 96% rename from src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilLoreEditUtil.kt rename to src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilLoreEditUtil.kt index 16eca40..edb4a77 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilLoreEditUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilLoreEditUtil.kt @@ -1,4 +1,4 @@ -package xyz.alexcrea.cuanvil.util +package xyz.alexcrea.cuanvil.util.anvil import net.kyori.adventure.text.Component import org.bukkit.entity.HumanEntity @@ -8,7 +8,7 @@ import org.bukkit.permissions.Permissible 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.AnvilXpUtil.AnvilCost +import xyz.alexcrea.cuanvil.util.MiniMessageUtil import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil import xyz.alexcrea.cuanvil.util.config.LoreEditType import java.util.* @@ -31,7 +31,7 @@ object AnvilLoreEditUtil { player: Permissible, first: ItemStack, book: BookMeta, - cost: AnvilCost + cost: AnvilXpUtil.AnvilCost ): ItemStack? { if (!hasLoreEditByBookPermission(player)) return null @@ -60,7 +60,7 @@ object AnvilLoreEditUtil { return result } - fun handleLoreRemoveByBook(player: Permissible, first: ItemStack, cost: AnvilCost): ItemStack? { + fun handleLoreRemoveByBook(player: Permissible, first: ItemStack, cost: AnvilXpUtil.AnvilCost): ItemStack? { if (!hasLoreEditByBookPermission(player)) return null // remove lore @@ -116,7 +116,7 @@ object AnvilLoreEditUtil { return null } - fun tryLoreEditByBook(player: HumanEntity, first: ItemStack, second: ItemStack, cost: AnvilCost): ItemStack? { + fun tryLoreEditByBook(player: HumanEntity, first: ItemStack, second: ItemStack, cost: AnvilXpUtil.AnvilCost): ItemStack? { val isAppend = bookLoreEditIsAppend(first, second) ?: return null val meta = second.itemMeta as BookMeta @@ -147,7 +147,7 @@ object AnvilLoreEditUtil { player: Permissible, first: ItemStack, second: ItemStack, - cost: AnvilCost + cost: AnvilXpUtil.AnvilCost ): ItemStack? { if (!hasLoreEditByPaperPermission(player)) return null @@ -181,7 +181,7 @@ object AnvilLoreEditUtil { return result } - fun handleLoreRemoveByPaper(player: Permissible, first: ItemStack, cost: AnvilCost): ItemStack? { + fun handleLoreRemoveByPaper(player: Permissible, first: ItemStack, cost: AnvilXpUtil.AnvilCost): ItemStack? { if (!hasLoreEditByPaperPermission(player)) return null // remove lore line @@ -223,7 +223,7 @@ object AnvilLoreEditUtil { player: HumanEntity, first: ItemStack, second: ItemStack, - cost: AnvilCost + cost: AnvilXpUtil.AnvilCost ): ItemStack? { val isAppend = paperLoreEditIsAppend(first, second) ?: return null @@ -232,7 +232,7 @@ object AnvilLoreEditUtil { } private fun baseEditLoreXpCost( - cost: AnvilCost, + cost: AnvilXpUtil.AnvilCost, first: ItemStack, result: ItemStack, editType: LoreEditType diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilUseTypeUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilUseTypeUtil.kt similarity index 96% rename from src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilUseTypeUtil.kt rename to src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilUseTypeUtil.kt index c72a35a..a9c7f39 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilUseTypeUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilUseTypeUtil.kt @@ -1,4 +1,4 @@ -package xyz.alexcrea.cuanvil.util +package xyz.alexcrea.cuanvil.util.anvil import io.delilaheve.util.ConfigOptions diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt similarity index 98% rename from src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt rename to src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt index cedc07c..0f94661 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilXpUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt @@ -1,8 +1,7 @@ -package xyz.alexcrea.cuanvil.util +package xyz.alexcrea.cuanvil.util.anvil import io.delilaheve.CustomAnvil import io.delilaheve.util.ConfigOptions -import io.delilaheve.util.ConfigOptions.getMonetaryMultiplier as moneyMultiplier import io.delilaheve.util.EnchantmentUtil.enchantmentName import io.delilaheve.util.ItemUtil.findEnchantments import io.delilaheve.util.ItemUtil.isEnchantedBook @@ -15,13 +14,16 @@ 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.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 import java.math.BigDecimal import kotlin.math.min +import io.delilaheve.util.ConfigOptions.getMonetaryMultiplier as moneyMultiplier object AnvilXpUtil { diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/config/LoreEditType.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/config/LoreEditType.kt index 8bd926a..8dab543 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/config/LoreEditType.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/config/LoreEditType.kt @@ -1,6 +1,6 @@ package xyz.alexcrea.cuanvil.util.config -import xyz.alexcrea.cuanvil.util.AnvilUseType +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 diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/dialog/AnvilRenameDialogUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/dialog/AnvilRenameDialogUtil.kt index a83d8b0..07d4e08 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/dialog/AnvilRenameDialogUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/dialog/AnvilRenameDialogUtil.kt @@ -8,7 +8,7 @@ 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.AnvilColorUtil +import xyz.alexcrea.cuanvil.util.anvil.AnvilColorUtil object AnvilRenameDialogUtil { From e6293be1c68dc93a86f6afa689669a9b638245ff Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 2 Jun 2026 14:01:49 +0200 Subject: [PATCH 130/166] deduplicate unit repair logic --- .../alexcrea/cuanvil/anvil/AnvilMergeLogic.kt | 9 ++ .../cuanvil/listener/AnvilResultListener.kt | 108 ++++++++---------- 2 files changed, 57 insertions(+), 60 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt index 9840cb8..ce0fe0d 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt @@ -225,6 +225,15 @@ object AnvilMergeUtil { ): UnitRepairResult { val unitRepairAmount = first.getRepair(second) ?: return UnitRepairResult.EMPTY + return testUnitRepair(inventory, player, first, second, unitRepairAmount) + } + + fun testUnitRepair( + 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) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt index 7497e7f..2235183 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -3,7 +3,6 @@ package xyz.alexcrea.cuanvil.listener import io.delilaheve.CustomAnvil import io.delilaheve.util.ConfigOptions import io.delilaheve.util.ItemUtil.canMergeWith -import io.delilaheve.util.ItemUtil.unitRepair import org.bukkit.GameMode import org.bukkit.Material import org.bukkit.entity.Player @@ -16,7 +15,8 @@ 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.AnvilUseType +import xyz.alexcrea.cuanvil.anvil.AnvilMergeUtil +import xyz.alexcrea.cuanvil.anvil.AnvilMergeUtil.AnvilResult import xyz.alexcrea.cuanvil.dependency.DependencyManager import xyz.alexcrea.cuanvil.dependency.economy.EconomyManager import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil.setComponentDisplayName @@ -65,8 +65,9 @@ class AnvilResultListener : Listener { 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) { - event.result = Event.Result.DENY return } @@ -83,7 +84,6 @@ class AnvilResultListener : Listener { // Do not continue if there was no change if ((output == inventory.getItem(ANVIL_INPUT_LEFT))) { - event.result = Event.Result.DENY return } @@ -102,10 +102,10 @@ class AnvilResultListener : Listener { } // Unit repair - val unitRepairResult = leftItem.getRepair(rightItem) + val unitRepairResult = leftItem.getRepair(rightItem) // Maybe this should be handlded "above" and like prepare result if (unitRepairResult != null) { onUnitRepairExtract( - leftItem, rightItem, output, + leftItem, rightItem, unitRepairResult, event, player, inventory ) return @@ -238,6 +238,25 @@ class AnvilResultListener : Listener { return true } + // I don't know about side effect of "handling cost" at this level and not before so let be safe + 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 + + processCost(inventory, player, result.cost) + + return extractAnvilResult(event, player, inventory, leftItem, leftRemoveCount, rightItem, + rightRemoveCount, result.item!!, result.cost) + } + private fun extractAnvilResult( event: InventoryClickEvent, player: Player, @@ -290,59 +309,7 @@ class AnvilResultListener : Listener { return true } - private fun onUnitRepairExtract( - leftItem: ItemStack, - rightItem: ItemStack, - output: ItemStack, - unitRepairResult: Double, - event: InventoryClickEvent, - player: Player, - inventory: AnvilInventory - ) { - val resultCopy = leftItem.clone() - val resultAmount = resultCopy.unitRepair( - rightItem.amount, unitRepairResult - ) - - // Get repair cost - val repairCost = getUnitRepairCost(inventory, player, leftItem, output, resultCopy, resultAmount) - - // And then we give the item manually - extractAnvilResult( - event, player, inventory, - null, 0, - rightItem, resultAmount, - resultCopy, repairCost - ) - } - - private fun getUnitRepairCost( - inventory: AnvilInventory, player: Player, - leftItem: ItemStack, output: ItemStack, - resultCopy: ItemStack, resultAmount: Int - ): AnvilCost { - if (player.gameMode == GameMode.CREATIVE) return AnvilCost(0) - - val cost = AnvilCost() - // Get repairCost - leftItem.itemMeta?.let { leftMeta -> - val leftName = leftMeta.displayName - output.itemMeta?.let { - // Rename cost - if (!leftName.contentEquals(it.displayName)) { - cost.rename += ConfigOptions.itemRenameCost - - // Color cost - if (it.displayName.contains('§')) { - cost.rename += ConfigOptions.useOfColorCost - } - } - } - } - - cost.workPenalty = AnvilXpUtil.calculatePenalty(leftItem, null, resultCopy, AnvilUseType.UNIT_REPAIR) - cost.repair = resultAmount * ConfigOptions.unitRepairCost - + private fun processCost(inventory: AnvilInventory, player: Player, cost: AnvilCost) { var sum = cost.repair if ( @@ -364,8 +331,29 @@ class AnvilResultListener : Listener { || (player.level < sum) ) cost.valid = false } + } - return cost + private fun onUnitRepairExtract( + leftItem: ItemStack, + rightItem: ItemStack, + unitRepairResult: Double, + event: InventoryClickEvent, + player: Player, + inventory: AnvilInventory + ) { + val result = AnvilMergeUtil.testUnitRepair(inventory, player, + leftItem.clone(), rightItem, + unitRepairResult) + + if(result.isEmpty()) return + + // And then we give the item manually + extractAnvilResult( + event, player, inventory, + null, 0, + rightItem, result.repairAmount, + result + ) } private fun getFromLoreEditXpCost( From 106bc724a13f350f87aeccbbba9478bd039b84d9 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 2 Jun 2026 14:36:09 +0200 Subject: [PATCH 131/166] deduplicate lore edit logic --- .../alexcrea/cuanvil/anvil/AnvilMergeLogic.kt | 38 +- .../cuanvil/listener/AnvilResultListener.kt | 328 +++++++++--------- .../cuanvil/listener/PrepareAnvilListener.kt | 12 +- .../cuanvil/util/anvil/AnvilLoreEditUtil.kt | 37 +- .../cuanvil/util/config/LoreEditType.kt | 13 +- 5 files changed, 230 insertions(+), 198 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt index ce0fe0d..b7d8bdb 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt @@ -28,9 +28,10 @@ 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.anvil.AnvilXpUtil.AnvilCost +import xyz.alexcrea.cuanvil.util.config.LoreEditType import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil -object AnvilMergeUtil { +object AnvilMergeLogic { open class AnvilResult { companion object { @@ -64,6 +65,19 @@ object AnvilMergeUtil { } } + + 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(inventory: AnvilInventory, player: Player, first: ItemStack ): AnvilResult { @@ -258,23 +272,23 @@ object AnvilMergeUtil { fun testLoreEdit( player: Player, first: ItemStack, second: ItemStack - ): AnvilResult { + ): LoreEditResult { val type = second.type - var resultItem: ItemStack? = null - val cost = AnvilCost() - if (Material.WRITABLE_BOOK == type) { - resultItem = AnvilLoreEditUtil.tryLoreEditByBook(player, first, second, cost) - } else if (Material.PAPER == type) { - resultItem = AnvilLoreEditUtil.tryLoreEditByPaper(player, first, second, cost) - } + 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 (resultItem.isAir || first == resultItem) { + if(result.isEmpty()) return result + + if (result.item!!.isAir || first == result.item) { CustomAnvil.log("lore edit, But input is same as output") - return AnvilResult.EMPTY + return LoreEditResult.EMPTY } - return AnvilResult(resultItem, cost) + return result } } \ 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 index 2235183..3e0371a 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -15,8 +15,9 @@ 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.AnvilMergeUtil -import xyz.alexcrea.cuanvil.anvil.AnvilMergeUtil.AnvilResult +import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic +import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.AnvilResult +import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.LoreEditResult import xyz.alexcrea.cuanvil.dependency.DependencyManager import xyz.alexcrea.cuanvil.dependency.economy.EconomyManager import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil.setComponentDisplayName @@ -112,14 +113,14 @@ class AnvilResultListener : Listener { } // For lore edit - if (handleBookLoreEdit(event, inventory, player, leftItem, rightItem, output)) { - return - } else if (handlePaperLoreEdit(event, inventory, player, leftItem, rightItem, output)) { + 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 } - - // Else there was no working situation somehow so we deny - event.result = Event.Result.DENY } private fun onCustomCraft( @@ -341,7 +342,7 @@ class AnvilResultListener : Listener { player: Player, inventory: AnvilInventory ) { - val result = AnvilMergeUtil.testUnitRepair(inventory, player, + val result = AnvilMergeLogic.testUnitRepair(inventory, player, leftItem.clone(), rightItem, unitRepairResult) @@ -356,99 +357,90 @@ class AnvilResultListener : Listener { ) } - private fun getFromLoreEditXpCost( - cost: AnvilCost, - player: Player, - inventory: AnvilInventory, - ): AnvilCost { - if (GameMode.CREATIVE == player.gameMode) return AnvilCost(0) - - if (ConfigOptions.shouldUseMoney(player)) { - cost.isMonetary = true - if (!EconomyManager.economy!!.has(player, cost.asMonetaryCost())) - cost.valid = false - } else { - val repairCost = cost.asXpCost() - - if ((inventory.maximumRepairCost <= repairCost) - || (player.level < repairCost) - ) - cost.valid = false - } - - return cost - } - private fun handleBookLoreEdit( event: InventoryClickEvent, inventory: AnvilInventory, player: Player, leftItem: ItemStack, rightItem: ItemStack, - output: ItemStack, - ): Boolean { - if (Material.WRITABLE_BOOK != rightItem.type) return false - val bookMeta = rightItem.itemMeta as BookMeta? ?: return false + result: LoreEditResult + ) { + if (result.type.isAppend) + handleBookLoreAppend(event, inventory, player, rightItem, result) + else + handleBookLoreRemove(event, inventory, player, leftItem, rightItem, result) + } - val editType = AnvilLoreEditUtil.bookLoreEditIsAppend(leftItem, rightItem) ?: return false + private fun handleBookLoreAppend( + event: InventoryClickEvent, + inventory: AnvilInventory, + player: Player, + rightItem: ItemStack, + result: LoreEditResult + ) { + val bookMeta = rightItem.itemMeta as BookMeta? ?: return - val cost = AnvilCost() - if (editType) { - if (output != AnvilLoreEditUtil.handleLoreAppendByBook(player, leftItem, bookMeta, cost)) return false - - // 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 - } - - return extractAnvilResult( - event, player, inventory, - null, 0, - clearedBook, 0, - output, getFromLoreEditXpCost(cost, player, inventory) - ) + // Remove pages to book + val clearedBook: ItemStack? + if (LoreEditType.APPEND_BOOK.doConsume) { + clearedBook = null } else { - if (output != AnvilLoreEditUtil.handleLoreRemoveByBook(player, leftItem, cost)) return false + clearedBook = rightItem.clone() + bookMeta.pages = Collections.emptyList() + clearedBook.itemMeta = bookMeta + } - // fill book meta - val lore = DependencyManager.stripLore(leftItem) - if (lore.isEmpty()) return false + extractAnvilResult( + event, player, inventory, + null, 0, + clearedBook, 0, + result + ) + } - val rightCopy: ItemStack? - if (LoreEditType.REMOVE_BOOK.doConsume) { - rightCopy = null - } else { - // Uncolor the page - AnvilLoreEditUtil.uncolorLines(player, lore, LoreEditType.REMOVE_BOOK) + private fun handleBookLoreRemove( + event: InventoryClickEvent, + inventory: AnvilInventory, + player: Player, + leftItem: ItemStack, + rightItem: ItemStack, + result: LoreEditResult + ){ + val bookMeta = rightItem.itemMeta as BookMeta? ?: return - val bookPage = StringBuilder() - lore.forEach { - if (bookPage.isNotEmpty()) bookPage.append('\n') - if (it == null) return@forEach + // fill book meta + val lore = DependencyManager.stripLore(leftItem) + if (lore.isEmpty()) return - bookPage.append(MiniMessageUtil.plain_text_mm.serialize(it)) - } + val rightCopy: ItemStack? + if (LoreEditType.REMOVE_BOOK.doConsume) { + rightCopy = null + } else { + // Uncolor the page + AnvilLoreEditUtil.uncolorLines(player, lore, LoreEditType.REMOVE_BOOK) - val resultPage = bookPage.toString() - //TODO maybe check page size ? bc it may be too big ??? + val bookPage = StringBuilder() + lore.forEach { + if (bookPage.isNotEmpty()) bookPage.append('\n') + if (it == null) return@forEach - rightCopy = rightItem.clone() - bookMeta.setPages(resultPage) - rightCopy.itemMeta = bookMeta + bookPage.append(MiniMessageUtil.plain_text_mm.serialize(it)) } - return extractAnvilResult( - event, player, inventory, - null, 0, - rightCopy, 0, - output, getFromLoreEditXpCost(cost, player, inventory) - ) + 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( @@ -457,89 +449,101 @@ class AnvilResultListener : Listener { player: Player, leftItem: ItemStack, rightItem: ItemStack, - output: ItemStack, - ): Boolean { - if (Material.PAPER != rightItem.type) return false - val paperMeta = rightItem.itemMeta ?: return false + result: LoreEditResult + ) { + if (result.type.isAppend) + handlePaperLoreAppend(event, inventory, player, rightItem, result) + else + handlePaperLoreRemove(event, inventory, player, leftItem, rightItem, result) + } - val editTypeIsAppend = AnvilLoreEditUtil.paperLoreEditIsAppend(leftItem, rightItem) ?: return false + private fun handlePaperLoreAppend( + event: InventoryClickEvent, + inventory: AnvilInventory, + player: Player, + rightItem: ItemStack, + result: LoreEditResult + ) { + val paperMeta = rightItem.itemMeta ?: return - val cost = AnvilCost() - if (editTypeIsAppend) { - if (output != AnvilLoreEditUtil.handleLoreAppendByPaper(player, leftItem, rightItem, cost)) return false - - 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) - paperCopy.itemMeta = paperMeta - } - - return if (rightItem.amount > 1) { - extractAnvilResult( - event, player, inventory, - paperCopy, 0, - rightItem, 1, - output, getFromLoreEditXpCost(cost, player, inventory) - ) - } else { - extractAnvilResult( - event, player, inventory, - null, 0, - paperCopy, 0, - output, getFromLoreEditXpCost(cost, player, inventory) - ) - } + val paperCopy: ItemStack? + if (LoreEditType.APPEND_PAPER.doConsume) { + paperCopy = null } else { - if (output != AnvilLoreEditUtil.handleLoreRemoveByPaper(player, leftItem, cost)) return false - - val leftMeta = leftItem.itemMeta - if (leftMeta == null || !leftMeta.hasLore()) return false - val lore = DependencyManager.stripLore(leftItem) - if (lore.isEmpty()) return false - - // 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 false - resultMeta.setComponentDisplayName(ref.get()) - rightClone.itemMeta = resultMeta - } - - return if (rightItem.amount > 1) { - extractAnvilResult( - event, player, inventory, - rightClone, 0, - rightItem, 1, - output, getFromLoreEditXpCost(cost, player, inventory) - ) - } else { - extractAnvilResult( - event, player, inventory, - null, 0, - rightClone, 0, - output, getFromLoreEditXpCost(cost, player, inventory) - ) - } + // Remove custom name to paper + paperCopy = rightItem.clone() + paperCopy.amount = 1 + paperMeta.setComponentDisplayName(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 + ) + } } /** diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index 312baba..0c7121f 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -13,12 +13,12 @@ import org.bukkit.inventory.AnvilInventory import org.bukkit.inventory.ItemStack import org.bukkit.inventory.meta.EnchantmentStorageMeta import org.bukkit.inventory.meta.ItemMeta -import xyz.alexcrea.cuanvil.anvil.AnvilMergeUtil.AnvilResult -import xyz.alexcrea.cuanvil.anvil.AnvilMergeUtil.doMerge -import xyz.alexcrea.cuanvil.anvil.AnvilMergeUtil.doRenaming -import xyz.alexcrea.cuanvil.anvil.AnvilMergeUtil.testCustomRecipe -import xyz.alexcrea.cuanvil.anvil.AnvilMergeUtil.testLoreEdit -import xyz.alexcrea.cuanvil.anvil.AnvilMergeUtil.testUnitRepair +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.JustForEasierHotswapUtil import xyz.alexcrea.cuanvil.util.MaterialUtil.isAir diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilLoreEditUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilLoreEditUtil.kt index edb4a77..e9bb633 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilLoreEditUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilLoreEditUtil.kt @@ -5,10 +5,12 @@ 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.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.anvil.AnvilXpUtil.AnvilCost import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil import xyz.alexcrea.cuanvil.util.config.LoreEditType import java.util.* @@ -31,7 +33,7 @@ object AnvilLoreEditUtil { player: Permissible, first: ItemStack, book: BookMeta, - cost: AnvilXpUtil.AnvilCost + cost: AnvilCost ): ItemStack? { if (!hasLoreEditByBookPermission(player)) return null @@ -60,7 +62,7 @@ object AnvilLoreEditUtil { return result } - fun handleLoreRemoveByBook(player: Permissible, first: ItemStack, cost: AnvilXpUtil.AnvilCost): ItemStack? { + fun handleLoreRemoveByBook(player: Permissible, first: ItemStack, cost: AnvilCost): ItemStack? { if (!hasLoreEditByBookPermission(player)) return null // remove lore @@ -116,12 +118,17 @@ object AnvilLoreEditUtil { return null } - fun tryLoreEditByBook(player: HumanEntity, first: ItemStack, second: ItemStack, cost: AnvilXpUtil.AnvilCost): ItemStack? { - val isAppend = bookLoreEditIsAppend(first, second) ?: 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 - return if (isAppend) handleLoreAppendByBook(player, first, meta, cost) + 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 @@ -147,7 +154,7 @@ object AnvilLoreEditUtil { player: Permissible, first: ItemStack, second: ItemStack, - cost: AnvilXpUtil.AnvilCost + cost: AnvilCost ): ItemStack? { if (!hasLoreEditByPaperPermission(player)) return null @@ -181,7 +188,7 @@ object AnvilLoreEditUtil { return result } - fun handleLoreRemoveByPaper(player: Permissible, first: ItemStack, cost: AnvilXpUtil.AnvilCost): ItemStack? { + fun handleLoreRemoveByPaper(player: Permissible, first: ItemStack, cost: AnvilCost): ItemStack? { if (!hasLoreEditByPaperPermission(player)) return null // remove lore line @@ -222,17 +229,21 @@ object AnvilLoreEditUtil { fun tryLoreEditByPaper( player: HumanEntity, first: ItemStack, - second: ItemStack, - cost: AnvilXpUtil.AnvilCost - ): ItemStack? { - val isAppend = paperLoreEditIsAppend(first, second) ?: return null + second: ItemStack + ): LoreEditResult { + val isAppend = paperLoreEditIsAppend(first, second) ?: return LoreEditResult.EMPTY + val type = if(isAppend) LoreEditType.APPEND_BOOK else LoreEditType.REMOVE_BOOK - return if (isAppend) handleLoreAppendByPaper(player, first, second, cost) + 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: AnvilXpUtil.AnvilCost, + cost: AnvilCost, first: ItemStack, result: ItemStack, editType: LoreEditType diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/config/LoreEditType.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/config/LoreEditType.kt index 8dab543..c094c71 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/config/LoreEditType.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/config/LoreEditType.kt @@ -18,20 +18,23 @@ 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), - REMOVE_BOOK(AnvilUseType.LORE_EDIT_BOOK_REMOVE, false, true), - APPEND_PAPER(AnvilUseType.LORE_EDIT_PAPER_APPEND, true, false), - REMOVE_PAPER(AnvilUseType.LORE_EDIT_PAPER_REMOVE, false, false), + 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, isAppend, isMultiLine) + ) : this(useType.path, useType, + isPaper, isAppend, isMultiLine) /** * If this edit type is enabled From edceba879fc34f733cd4c61eeb5b0f0f4bedf618 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 2 Jun 2026 16:18:16 +0200 Subject: [PATCH 132/166] custom craft logic deduplication --- .../alexcrea/cuanvil/anvil/AnvilMergeLogic.kt | 27 ++++++++-- .../cuanvil/listener/AnvilResultListener.kt | 54 ++++++++----------- .../cuanvil/listener/PrepareAnvilListener.kt | 2 +- .../cuanvil/util/anvil/AnvilXpUtil.kt | 4 +- .../cuanvil/util/AnvilFuseTestUtil.java | 4 +- 5 files changed, 51 insertions(+), 40 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt index b7d8bdb..8699285 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt @@ -19,6 +19,7 @@ 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 @@ -28,6 +29,7 @@ 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.anvil.AnvilXpUtil.AnvilCost +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil.CustomCraftCost import xyz.alexcrea.cuanvil.util.config.LoreEditType import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil @@ -65,6 +67,22 @@ object AnvilMergeLogic { } } + 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 { @@ -211,10 +229,10 @@ object AnvilMergeLogic { fun testCustomRecipe( player: Player, first: ItemStack, second: ItemStack? - ): AnvilResult { + ): CustomCraftResult { val recipe = CustomRecipeUtil.getCustomRecipe(first, second) CustomAnvil.verboseLog("custom recipe not null? ${recipe != null}") - if (recipe == null) return AnvilResult.EMPTY + if (recipe == null) return CustomCraftResult.EMPTY val amount = CustomRecipeUtil.getCustomRecipeAmount(recipe, first, second) @@ -224,12 +242,13 @@ object AnvilMergeLogic { // Maybe add an option on custom craft to ignore/not ignore penalty ?? val xpCost = recipe.determineCost(amount, first, resultItem) - val cost = AnvilCost() + 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(resultItem, AnvilUseType.CUSTOM_CRAFT, cost) - return AnvilResult(result, cost, true) + return CustomCraftResult(result, cost, amount, recipe) } fun testUnitRepair( diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt index 3e0371a..d63e2a6 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -17,7 +17,9 @@ import org.bukkit.inventory.ItemStack import org.bukkit.inventory.meta.BookMeta 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 @@ -27,7 +29,6 @@ import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener.Companion.ANVIL_OUTPUT import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe import xyz.alexcrea.cuanvil.util.CustomRecipeUtil import xyz.alexcrea.cuanvil.util.MiniMessageUtil -import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair import xyz.alexcrea.cuanvil.util.anvil.AnvilLoreEditUtil import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil.AnvilCost @@ -73,12 +74,11 @@ class AnvilResultListener : Listener { } // Test custom recipe - val recipe = CustomRecipeUtil.getCustomRecipe(leftItem, rightItem) - if (recipe != null) { - event.result = Event.Result.ALLOW + val customRecipeResult = AnvilMergeLogic.testCustomRecipe(player, leftItem, rightItem) + if (!customRecipeResult.isEmpty()) { onCustomCraft( - event, recipe, player, - leftItem, rightItem, output, inventory + event, player, inventory, + leftItem, rightItem, customRecipeResult ) return } @@ -103,11 +103,13 @@ class AnvilResultListener : Listener { } // Unit repair - val unitRepairResult = leftItem.getRepair(rightItem) // Maybe this should be handlded "above" and like prepare result - if (unitRepairResult != null) { + val unitRepairResult = AnvilMergeLogic.testUnitRepair( + inventory, player, + leftItem, rightItem) + if (unitRepairResult.isEmpty()) { onUnitRepairExtract( - leftItem, rightItem, - unitRepairResult, event, player, inventory + rightItem, event, player, inventory, + unitRepairResult ) return } @@ -125,19 +127,14 @@ class AnvilResultListener : Listener { private fun onCustomCraft( event: InventoryClickEvent, - recipe: AnvilCustomRecipe, player: Player, + inventory: AnvilInventory, leftItem: ItemStack, rightItem: ItemStack?, - output: ItemStack, - inventory: AnvilInventory + result: CustomCraftResult, ) { - event.result = Event.Result.DENY - - if (recipe.leftItem == null) return // in case it changed - - val amount = CustomRecipeUtil.getCustomRecipeAmount(recipe, leftItem, rightItem) - val xpCost = recipe.determineCost(amount, leftItem, output) + val recipe = result.recipe!! + val xpCost = result.customCraftCost.rawCost val finalCost = if (recipe.removeExactLinearXp) xpCost else AnvilXpUtil.calculateLevelForXp(xpCost) @@ -166,7 +163,7 @@ class AnvilResultListener : Listener { player, leftItem, rightItem, - amount, + result.amount, finalCost, recipe.removeExactLinearXp ) @@ -174,9 +171,9 @@ class AnvilResultListener : Listener { // Finally, we add the item to the player if (slotDestination.type == SlotType.CURSOR) { - player.setItemOnCursor(output) + player.setItemOnCursor(result.item) } else {// We assume SlotType == SlotType.INVENTORY - player.inventory.setItem(slotDestination.slot, output) + player.inventory.setItem(slotDestination.slot, result.item) } } @@ -335,20 +332,13 @@ class AnvilResultListener : Listener { } private fun onUnitRepairExtract( - leftItem: ItemStack, rightItem: ItemStack, - unitRepairResult: Double, event: InventoryClickEvent, player: Player, - inventory: AnvilInventory + inventory: AnvilInventory, + result: UnitRepairResult, ) { - val result = AnvilMergeLogic.testUnitRepair(inventory, player, - leftItem.clone(), rightItem, - unitRepairResult) - - if(result.isEmpty()) return - - // And then we give the item manually + // We give the item manually extractAnvilResult( event, player, inventory, null, 0, diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index 0c7121f..00a2053 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -104,7 +104,7 @@ class PrepareAnvilListener : Listener { return AnvilResult.EMPTY // Test custom recipe - var result = testCustomRecipe(player, first, second) + var result: AnvilResult = testCustomRecipe(player, first, second) if (!result.isEmpty()) return result diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt index 0f94661..997b260 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt @@ -29,7 +29,7 @@ object AnvilXpUtil { const val EXCLUSIVE_PENALTY_PREFIX = "repair_cost" - class AnvilCost { + open class AnvilCost { private val isAlone: Boolean var valid = true // Get set as invalid if cost can be satisfied var isMonetary = false @@ -71,6 +71,8 @@ object AnvilXpUtil { } } + class CustomCraftCost(val rawCost: Int): AnvilCost() + /** * Display the required cost (either as xp or as ) */ diff --git a/src/test/java/xyz/alexcrea/cuanvil/util/AnvilFuseTestUtil.java b/src/test/java/xyz/alexcrea/cuanvil/util/AnvilFuseTestUtil.java index 1d3c5f4..6f5c7bb 100644 --- a/src/test/java/xyz/alexcrea/cuanvil/util/AnvilFuseTestUtil.java +++ b/src/test/java/xyz/alexcrea/cuanvil/util/AnvilFuseTestUtil.java @@ -195,7 +195,7 @@ public class AnvilFuseTestUtil { simulateClick(anvil, player, data.expectedResult()); - // Should have similated the click + // Should have simulated the click assertEqual(data.leftItem(), anvil.getFirstItem()); assertEqual(data.rightItem(), anvil.getSecondItem()); assertEqual(data.resultSlotItem(), anvil.getResult()); @@ -260,7 +260,7 @@ public class AnvilFuseTestUtil { } public static boolean isAir(@Nullable ItemStack item) { - return item == null || item.isEmpty(); + return item == null || item.isEmpty() || item.getAmount() == 0; } public static void assertPriceEqual(Integer expectedPrice, int price) { From d0078e528d91bf841f350500a235e64d1391ae4d Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 2 Jun 2026 16:50:14 +0200 Subject: [PATCH 133/166] fix creative price and other small fixes --- .../cuanvil/listener/AnvilResultListener.kt | 66 ++++++++++--------- .../cuanvil/listener/PrepareAnvilListener.kt | 4 +- .../cuanvil/util/JustForEasierHotswapUtil.kt | 17 ----- 3 files changed, 38 insertions(+), 49 deletions(-) delete mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/util/JustForEasierHotswapUtil.kt diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt index d63e2a6..552e777 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -90,15 +90,29 @@ class AnvilResultListener : Listener { // Rename if (rightItem == null) { - // BRUH - event.result = Event.Result.ALLOW + val result = AnvilMergeLogic.doRenaming(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) { - event.result = Event.Result.ALLOW + val result = AnvilMergeLogic.doMerge(inventory, player, leftItem, rightItem) + + extractAnvilResult( + event, player, inventory, + null, 0, + null, 0, + result + ) return } @@ -195,6 +209,7 @@ class AnvilResultListener : Listener { inventory.setItem(ANVIL_INPUT_LEFT, leftItem) if (player.gameMode != GameMode.CREATIVE) { + //TODO monetary cost ? somehow if (linearCost) { val levelXp = AnvilXpUtil.calculateXpForLevel(player.level) val delta = AnvilXpUtil.calculateXpForLevel(player.level + 1) - levelXp @@ -236,7 +251,18 @@ class AnvilResultListener : Listener { return true } - // I don't know about side effect of "handling cost" at this level and not before so let be safe + private fun tryRemoveCost(player: Player, cost: AnvilCost): Boolean { + if(player.gameMode == GameMode.CREATIVE) return true + if(cost.isMonetary) { + val result = EconomyManager.economy!!.remove(player, cost.asMonetaryCost()) + if(!result) return false + } else { + player.level -= cost.asXpCost() + } + + return true + } + private fun extractAnvilResult( event: InventoryClickEvent, player: Player, @@ -249,28 +275,13 @@ class AnvilResultListener : Listener { ): Boolean { if(result.isEmpty()) return false - processCost(inventory, player, result.cost) - - return extractAnvilResult(event, player, inventory, leftItem, leftRemoveCount, rightItem, - rightRemoveCount, result.item!!, result.cost) - } - - private fun extractAnvilResult( - event: InventoryClickEvent, - player: Player, - inventory: AnvilInventory, - leftItem: ItemStack?, - leftRemoveCount: Int, - rightItem: ItemStack?, - rightRemoveCount: Int, - output: ItemStack, - cost: AnvilCost, - ): Boolean { // To avoid vanilla, we cancel the event event.result = Event.Result.DENY event.isCancelled = true + val cost = result.cost - if (!cost.valid) return false + processCost(inventory, player, cost) + if (!cost.valid && player.gameMode != GameMode.CREATIVE) return false // Where should we get the item val slotDestination = getActionSlot(event, player) @@ -278,12 +289,7 @@ class AnvilResultListener : Listener { // If not creative middle click... if (event.click != ClickType.MIDDLE) { - if(cost.isMonetary) { - val result = EconomyManager.economy!!.remove(player, cost.asMonetaryCost()) - if(!result) return false - } else { - player.level -= cost.asXpCost() - } + if(!tryRemoveCost(player, cost)) return false // We remove what should be removed if (leftItem != null) leftItem.amount -= leftRemoveCount @@ -298,9 +304,9 @@ class AnvilResultListener : Listener { // Finally, we add the item to the player if (SlotType.CURSOR == slotDestination.type) { - player.setItemOnCursor(output) + player.setItemOnCursor(result.item) } else {// We assume SlotType == SlotType.INVENTORY - player.inventory.setItem(slotDestination.slot, output) + player.inventory.setItem(slotDestination.slot, result.item) } // TODO probably anvil damage & sound here ?? diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index 00a2053..21438ae 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -1,5 +1,6 @@ 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 @@ -20,7 +21,6 @@ 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.JustForEasierHotswapUtil import xyz.alexcrea.cuanvil.util.MaterialUtil.isAir import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil.AnvilCost @@ -49,7 +49,7 @@ class PrepareAnvilListener : Listener { val view = event.view val inventory = event.inventory - val player = JustForEasierHotswapUtil.getPlayerFromView(view) + val player = InventoryViewUtil.getInstance().getPlayer(view) if(player !is Player) return tryRenameDialog(player, event) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/JustForEasierHotswapUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/JustForEasierHotswapUtil.kt deleted file mode 100644 index ea0f7ec..0000000 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/JustForEasierHotswapUtil.kt +++ /dev/null @@ -1,17 +0,0 @@ -package xyz.alexcrea.cuanvil.util - -import com.github.stefvanschie.inventoryframework.util.InventoryViewUtil -import org.bukkit.entity.HumanEntity -import org.bukkit.inventory.InventoryView - -// Hotswap to not relocate -// So I just put small thing calling relocating method here to enable to hotswap more class -// Especially for PrepareAnvilListener -// Will be able to replace that on legacy removal so really temporary -object JustForEasierHotswapUtil { - - fun getPlayerFromView(view: InventoryView): HumanEntity { - return InventoryViewUtil.getInstance().getPlayer(view) - } - -} \ No newline at end of file From 2768c0a0dc77972e449e834e0a3fb101d589ae17 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 2 Jun 2026 23:53:18 +0200 Subject: [PATCH 134/166] custom craft monetary cost fixed --- .../cuanvil/listener/AnvilResultListener.kt | 80 +++++++++++-------- .../cuanvil/util/anvil/AnvilXpUtil.kt | 10 ++- 2 files changed, 56 insertions(+), 34 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt index 552e777..549fd9a 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -26,7 +26,6 @@ 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.recipe.AnvilCustomRecipe import xyz.alexcrea.cuanvil.util.CustomRecipeUtil import xyz.alexcrea.cuanvil.util.MiniMessageUtil import xyz.alexcrea.cuanvil.util.anvil.AnvilLoreEditUtil @@ -34,6 +33,7 @@ import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil.AnvilCost import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil import xyz.alexcrea.cuanvil.util.config.LoreEditType +import java.math.BigDecimal import java.util.* import java.util.concurrent.atomic.AtomicReference import kotlin.math.min @@ -148,14 +148,20 @@ class AnvilResultListener : Listener { result: CustomCraftResult, ) { val recipe = result.recipe!! - val xpCost = result.customCraftCost.rawCost + val rawCost = result.customCraftCost.rawCost val finalCost = - if (recipe.removeExactLinearXp) xpCost - else AnvilXpUtil.calculateLevelForXp(xpCost) + 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}") - 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 (recipe.removeExactLinearXp) { + if(ConfigOptions.shouldUseMoney(player)) { + result.cost.isMonetary = true + if(!EconomyManager.economy!!.has(player, BigDecimal(rawCost))) 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 @@ -172,14 +178,11 @@ class AnvilResultListener : Listener { if (event.click != ClickType.MIDDLE && !handleCustomCraftClick( event, - recipe, inventory, player, leftItem, rightItem, - result.amount, - finalCost, - recipe.removeExactLinearXp + result ) ) return @@ -192,11 +195,14 @@ class AnvilResultListener : Listener { } private fun handleCustomCraftClick( - event: InventoryClickEvent, recipe: AnvilCustomRecipe, + event: InventoryClickEvent, inventory: AnvilInventory, player: Player, leftItem: ItemStack, rightItem: ItemStack?, - amount: Int, xpCost: Int, linearCost: Boolean = false + 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 @@ -208,26 +214,7 @@ class AnvilResultListener : Listener { leftItem.amount -= amount * recipe.leftItem!!.amount inventory.setItem(ANVIL_INPUT_LEFT, leftItem) - if (player.gameMode != GameMode.CREATIVE) { - //TODO monetary cost ? somehow - if (linearCost) { - val levelXp = AnvilXpUtil.calculateXpForLevel(player.level) - val delta = AnvilXpUtil.calculateXpForLevel(player.level + 1) - levelXp - var totalXp = levelXp + player.exp * delta - totalXp -= xpCost - - 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 -= xpCost - } - } + removeCustomCraftCost(player, result) // Then we try to find the new values for the anvil val newAmount = CustomRecipeUtil.getCustomRecipeAmount(recipe, leftItem, rightItem) @@ -251,6 +238,35 @@ class AnvilResultListener : Listener { 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, BigDecimal(rawCost)) + 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 -= rawCost + } + + } + private fun tryRemoveCost(player: Player, cost: AnvilCost): Boolean { if(player.gameMode == GameMode.CREATIVE) return true if(cost.isMonetary) { diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt index 997b260..b57a696 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt @@ -56,7 +56,7 @@ object AnvilXpUtil { return generic + enchantment + repair + rename + lore + illegalPenalty + workPenalty + recipe } - fun asMonetaryCost(): BigDecimal { + open fun asMonetaryCost(): BigDecimal { // multiply by per use type multipliers return BigDecimal(generic) .add(BigDecimal(enchantment).multiply(moneyMultiplier("enchantment"))) @@ -71,7 +71,13 @@ object AnvilXpUtil { } } - class CustomCraftCost(val rawCost: Int): AnvilCost() + class CustomCraftCost(val rawCost: Int): AnvilCost() { + + override fun asMonetaryCost(): BigDecimal { + return BigDecimal(rawCost) + } + + } /** * Display the required cost (either as xp or as ) From bf4395ba3f2d21ecc515e4a319a6c721ba3b38f4 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Wed, 3 Jun 2026 02:27:20 +0200 Subject: [PATCH 135/166] fix multiples issues --- defaultconfigs/1.18/config.yml | 5 ++++- defaultconfigs/1.21.11/config.yml | 5 ++++- defaultconfigs/1.21.9/config.yml | 5 ++++- defaultconfigs/1.21/config.yml | 5 ++++- .../xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt | 6 +++--- .../kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt | 1 + src/main/resources/config.yml | 5 ++++- 7 files changed, 24 insertions(+), 8 deletions(-) diff --git a/defaultconfigs/1.18/config.yml b/defaultconfigs/1.18/config.yml index fb70505..b5ad3b0 100644 --- a/defaultconfigs/1.18/config.yml +++ b/defaultconfigs/1.18/config.yml @@ -436,7 +436,10 @@ lore_edit: # 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 # -# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 +# 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 diff --git a/defaultconfigs/1.21.11/config.yml b/defaultconfigs/1.21.11/config.yml index b70798c..44885b7 100644 --- a/defaultconfigs/1.21.11/config.yml +++ b/defaultconfigs/1.21.11/config.yml @@ -456,7 +456,10 @@ lore_edit: # 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 # -# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 +# 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 diff --git a/defaultconfigs/1.21.9/config.yml b/defaultconfigs/1.21.9/config.yml index 31bdb1f..4df5876 100644 --- a/defaultconfigs/1.21.9/config.yml +++ b/defaultconfigs/1.21.9/config.yml @@ -448,7 +448,10 @@ lore_edit: # 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 # -# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 +# 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 diff --git a/defaultconfigs/1.21/config.yml b/defaultconfigs/1.21/config.yml index 1dafa54..5d59e5a 100644 --- a/defaultconfigs/1.21/config.yml +++ b/defaultconfigs/1.21/config.yml @@ -436,7 +436,10 @@ lore_edit: # 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 # -# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 +# 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 diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt index 549fd9a..6b5f510 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -160,7 +160,7 @@ class AnvilResultListener : Listener { if (player.gameMode != GameMode.CREATIVE) { if(ConfigOptions.shouldUseMoney(player)) { result.cost.isMonetary = true - if(!EconomyManager.economy!!.has(player, BigDecimal(rawCost))) return + 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 @@ -243,7 +243,7 @@ class AnvilResultListener : Listener { val rawCost = result.customCraftCost.rawCost if(result.cost.isMonetary) { - EconomyManager.economy!!.remove(player, BigDecimal(rawCost)) + EconomyManager.economy!!.remove(player, result.cost.asMonetaryCost()) return } @@ -262,7 +262,7 @@ class AnvilResultListener : Listener { player.level = newLevel player.exp = xp / newDelta } else { - player.level -= rawCost + player.level -= AnvilXpUtil.calculateLevelForXp(rawCost) } } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt index b57a696..8c39a0a 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt @@ -75,6 +75,7 @@ object AnvilXpUtil { override fun asMonetaryCost(): BigDecimal { return BigDecimal(rawCost) + .multiply(moneyMultiplier("global")) } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 27c0248..7d6e396 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -438,7 +438,10 @@ lore_edit: # 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 # -# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 +# 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 From 31fa3d38b7b3da88d9485195ebb82a7056795163 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sat, 6 Jun 2026 12:46:32 +0200 Subject: [PATCH 136/166] minimum version of datapack tester upped to unsure minimum java version --- .../alexcrea/cuanvil/dependency/datapack/DataPackDependency.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/datapack/DataPackDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/datapack/DataPackDependency.kt index b0e9e52..6d54456 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/datapack/DataPackDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/datapack/DataPackDependency.kt @@ -17,7 +17,7 @@ import xyz.alexcrea.cuanvil.update.Version import java.io.InputStreamReader object DataPackDependency { - private val START_DETECT_VERSION = Version(1, 19, 0) + private val START_DETECT_VERSION = Version(1, 20, 5) /** * Map of the latest CustomAnvil update related to the pack From 9f06f708f50e7c2d5c89012e41b9015828385f86 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 8 Jun 2026 10:28:03 +0200 Subject: [PATCH 137/166] fix superenchant price --- .../cuanvil/enchant/wrapped/CASuperEnchantEnchantment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CASuperEnchantEnchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CASuperEnchantEnchantment.java index 669c1fa..f5943be 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CASuperEnchantEnchantment.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CASuperEnchantEnchantment.java @@ -24,7 +24,7 @@ public class CASuperEnchantEnchantment extends CAEnchantmentBase implements Addi private @NotNull EnchantManager enchantManager; public CASuperEnchantEnchantment(@NotNull CustomEnchant enchant, @NotNull Plugin plugin, @NotNull EnchantManager enchantManager) { - super(NamespacedKey.fromString(enchant.getId(), plugin), EnchantmentRarity.NO_RARITY, enchant.getMaxLevel()); + super(NamespacedKey.fromString(enchant.getId(), plugin), EnchantmentRarity.COMMON, enchant.getMaxLevel()); this.enchant = enchant; this.enchantManager = enchantManager; From d679cd73f98243735700aafe0f7556e433423aa1 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 9 Jun 2026 14:08:17 +0200 Subject: [PATCH 138/166] remove rename pdc on paper lore append --- .../alexcrea/cuanvil/anvil/AnvilMergeLogic.kt | 55 +++++++++++-------- .../cuanvil/listener/AnvilResultListener.kt | 39 +++++++------ 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt index 8699285..e018eff 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt @@ -55,7 +55,7 @@ object AnvilMergeLogic { } } - class UnitRepairResult: AnvilResult { + class UnitRepairResult : AnvilResult { companion object { val EMPTY = UnitRepairResult(null, AnvilCost(), 0) } @@ -67,7 +67,7 @@ object AnvilMergeLogic { } } - class CustomCraftResult: AnvilResult { + class CustomCraftResult : AnvilResult { companion object { val EMPTY = CustomCraftResult(null, CustomCraftCost(0), 0, null) } @@ -76,15 +76,17 @@ object AnvilMergeLogic { val amount: Int val recipe: AnvilCustomRecipe? - constructor(item: ItemStack?, cost: CustomCraftCost, - amount: Int, recipe: AnvilCustomRecipe?) : super(item, cost, true) { + 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 { + class LoreEditResult : AnvilResult { companion object { val EMPTY = LoreEditResult(null, AnvilCost(), LoreEditType.APPEND_PAPER) } @@ -96,8 +98,9 @@ object AnvilMergeLogic { } } - fun doRenaming(inventory: AnvilInventory, - player: Player, first: ItemStack + fun doRenaming( + inventory: AnvilInventory, + player: Player, first: ItemStack ): AnvilResult { val resultItem = DependencyManager.cloneItem(player, first) val cost = AnvilCost() @@ -115,15 +118,19 @@ object AnvilMergeLogic { return AnvilResult(result, cost) } - private fun processDialogPCD(it: ItemMeta, player: HumanEntity) { + 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 = it.persistentDataContainer - if(!keepDialog) + val pdc = meta.persistentDataContainer + if (!keepDialog) pdc.remove(AnvilRenameDialog.PCD_KEEP_RENAME_TEXT_KEY) else { - val text = AnvilRenameDialogUtil.anvilRenameDialog.currentText(player) - if(text == null || text.isBlank()) + 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) } @@ -138,7 +145,8 @@ object AnvilMergeLogic { if (ConfigOptions.renameColorPossible && renameText != null) { val component = AnvilColorUtil.handleColor( renameText, - AnvilColorUtil.renamePermission(player)) + AnvilColorUtil.renamePermission(player) + ) if (component != null) { renameText = MiniMessageUtil.legacy_mm.serialize(component) @@ -160,7 +168,8 @@ object AnvilMergeLogic { 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 @@ -184,7 +193,7 @@ object AnvilMergeLogic { val resultItem = DependencyManager.cloneItem(player, first) val cost = AnvilCost() - if(hasChanged){ + if (hasChanged) { resultItem.setEnchantmentsUnsafe(newEnchants) // Calculate enchantment cost AnvilXpUtil.getRightValues(second, resultItem, cost) @@ -217,9 +226,9 @@ object AnvilMergeLogic { firstEnchants: MutableMap, resultEnchants: MutableMap ): Boolean { - if(firstEnchants.size != resultEnchants.size) return false + if (firstEnchants.size != resultEnchants.size) return false for (entry in resultEnchants) { - if(firstEnchants.getOrDefault(entry.key, entry.value-1) != entry.value) return false + if (firstEnchants.getOrDefault(entry.key, entry.value - 1) != entry.value) return false } return true @@ -262,11 +271,11 @@ object AnvilMergeLogic { } fun testUnitRepair( - inventory: AnvilInventory, - player: Player, - first: ItemStack, second: ItemStack, - unitRepairAmount: Double - ): UnitRepairResult { + 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) @@ -300,7 +309,7 @@ object AnvilMergeLogic { AnvilLoreEditUtil.tryLoreEditByPaper(player, first, second) else LoreEditResult.EMPTY - if(result.isEmpty()) return result + if (result.isEmpty()) return result if (result.item!!.isAir || first == result.item) { CustomAnvil.log("lore edit, But input is same as output") diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt index 6b5f510..a232046 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -91,7 +91,7 @@ class AnvilResultListener : Listener { // Rename if (rightItem == null) { val result = AnvilMergeLogic.doRenaming(inventory, player, leftItem) - if(result.isEmpty()) return + if (result.isEmpty()) return extractAnvilResult( event, player, inventory, @@ -119,7 +119,8 @@ class AnvilResultListener : Listener { // Unit repair val unitRepairResult = AnvilMergeLogic.testUnitRepair( inventory, player, - leftItem, rightItem) + leftItem, rightItem + ) if (unitRepairResult.isEmpty()) { onUnitRepairExtract( rightItem, event, player, inventory, @@ -130,8 +131,8 @@ class AnvilResultListener : Listener { // For lore edit val loreResult = AnvilMergeLogic.testLoreEdit(player, leftItem, rightItem) - if(!loreResult.isEmpty()) { - if(loreResult.type.isBook) + if (!loreResult.isEmpty()) { + if (loreResult.type.isBook) handleBookLoreEdit(event, inventory, player, leftItem, rightItem, loreResult) else handlePaperLoreEdit(event, inventory, player, leftItem, rightItem, loreResult) @@ -153,14 +154,16 @@ class AnvilResultListener : Listener { 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}") + 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)) { + if (ConfigOptions.shouldUseMoney(player)) { result.cost.isMonetary = true - if(!EconomyManager.economy!!.has(player, result.cost.asMonetaryCost())) return + 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 @@ -242,7 +245,7 @@ class AnvilResultListener : Listener { if (player.gameMode == GameMode.CREATIVE) return val rawCost = result.customCraftCost.rawCost - if(result.cost.isMonetary) { + if (result.cost.isMonetary) { EconomyManager.economy!!.remove(player, result.cost.asMonetaryCost()) return } @@ -268,10 +271,10 @@ class AnvilResultListener : Listener { } private fun tryRemoveCost(player: Player, cost: AnvilCost): Boolean { - if(player.gameMode == GameMode.CREATIVE) return true - if(cost.isMonetary) { + if (player.gameMode == GameMode.CREATIVE) return true + if (cost.isMonetary) { val result = EconomyManager.economy!!.remove(player, cost.asMonetaryCost()) - if(!result) return false + if (!result) return false } else { player.level -= cost.asXpCost() } @@ -289,7 +292,7 @@ class AnvilResultListener : Listener { rightRemoveCount: Int, result: AnvilResult ): Boolean { - if(result.isEmpty()) return false + if (result.isEmpty()) return false // To avoid vanilla, we cancel the event event.result = Event.Result.DENY @@ -305,7 +308,7 @@ class AnvilResultListener : Listener { // If not creative middle click... if (event.click != ClickType.MIDDLE) { - if(!tryRemoveCost(player, cost)) return false + if (!tryRemoveCost(player, cost)) return false // We remove what should be removed if (leftItem != null) leftItem.amount -= leftRemoveCount @@ -417,7 +420,7 @@ class AnvilResultListener : Listener { leftItem: ItemStack, rightItem: ItemStack, result: LoreEditResult - ){ + ) { val bookMeta = rightItem.itemMeta as BookMeta? ?: return // fill book meta @@ -486,6 +489,10 @@ class AnvilResultListener : Listener { paperCopy = rightItem.clone() paperCopy.amount = 1 paperMeta.setComponentDisplayName(null) + + // Remove pcd name + AnvilMergeLogic.processPCD(paperMeta, player, null) + paperCopy.itemMeta = paperMeta } From 2efb6e55e285e23c83bcec8e66f6a286e7c1cadf Mon Sep 17 00:00:00 2001 From: alexcrea Date: Wed, 10 Jun 2026 14:54:13 +0200 Subject: [PATCH 139/166] new treat anvil event --- .../listener/CAClickResultBypassEvent.java | 2 +- .../listener/CAEarlyPreAnvilBypassEvent.java | 2 +- .../event/listener/CAPreAnvilBypassEvent.java | 2 +- .../listener/CATreatAnvilResult2Event.java | 196 ++++++++++++++++++ .../listener/CATreatAnvilResultEvent.java | 4 +- .../alexcrea/cuanvil/anvil/AnvilMergeLogic.kt | 17 +- .../cuanvil/dependency/DependencyManager.kt | 16 +- .../plugins/ExcellentEnchantsDependency.kt | 13 +- .../cuanvil/listener/AnvilResultListener.kt | 11 +- .../cuanvil/listener/PrepareAnvilListener.kt | 12 +- 10 files changed, 244 insertions(+), 31 deletions(-) create mode 100644 src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResult2Event.java 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 index a27c65e..fe5e199 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAClickResultBypassEvent.java +++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAClickResultBypassEvent.java @@ -17,7 +17,7 @@ import org.jetbrains.annotations.NotNull; * Most of the time you would likely need {@link CAPreAnvilBypassEvent} or {@link CAEarlyPreAnvilBypassEvent} * for this event to be useful. *

    - * There is also {@link CATreatAnvilResultEvent} that may be better for some use case. + * There is also {@link CATreatAnvilResult2Event} that may be better for some use case. */ public class CAClickResultBypassEvent extends Event implements Cancellable { 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 index 2fbd275..e92b4cd 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAEarlyPreAnvilBypassEvent.java +++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAEarlyPreAnvilBypassEvent.java @@ -15,7 +15,7 @@ import org.jetbrains.annotations.NotNull; *

    * 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 CATreatAnvilResultEvent} + * 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 { 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 index 18334e3..9103a4b 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAPreAnvilBypassEvent.java +++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAPreAnvilBypassEvent.java @@ -18,7 +18,7 @@ import org.jetbrains.annotations.NotNull; *

    * 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 CATreatAnvilResultEvent} + * 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 { 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..aa8f8e1 --- /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.AnvilUseType; +import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil; + +/** + * 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 AnvilXpUtil.AnvilCost cost; + + @ApiStatus.Internal + public CATreatAnvilResult2Event( + @NotNull InventoryView view, + Inventory inv, + AnvilUseType useType, + @Nullable ItemStack result, + AnvilXpUtil.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 AnvilXpUtil.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 index ddbaf23..6e6358d 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResultEvent.java +++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResultEvent.java @@ -19,8 +19,8 @@ import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil.AnvilCost; *

    * A null result will cancel this pre anvil event * - * @deprecated Prepare anvil Event should not be provided as it can be called on result and therefor not have prepare anvil event - * TODO a replacement is necessary but not yet made + * @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") diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt index e018eff..67b9b7d 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt @@ -13,6 +13,7 @@ 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 @@ -99,6 +100,7 @@ object AnvilMergeLogic { } fun doRenaming( + view: InventoryView, //TODO use anvil view inventory: AnvilInventory, player: Player, first: ItemStack ): AnvilResult { @@ -113,7 +115,7 @@ object AnvilMergeLogic { } cost.workPenalty = AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.RENAME_ONLY) - val result = DependencyManager.tryTreatAnvilResult(resultItem, AnvilUseType.RENAME_ONLY, cost) + val result = DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.RENAME_ONLY, cost) return AnvilResult(result, cost) } @@ -183,6 +185,7 @@ object AnvilMergeLogic { } fun doMerge( + view: InventoryView, //TODO use anvil view instead inventory: AnvilInventory, player: Player, first: ItemStack, second: ItemStack @@ -217,7 +220,7 @@ object AnvilMergeLogic { // Calculate rename cost cost.rename = handleRename(resultItem, inventory, player) - val result = DependencyManager.tryTreatAnvilResult(resultItem, AnvilUseType.MERGE, cost) + val result = DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.MERGE, cost) return AnvilResult(result, cost) } @@ -236,6 +239,8 @@ object AnvilMergeLogic { // 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 { @@ -256,21 +261,23 @@ object AnvilMergeLogic { cost.recipe = if (recipe.removeExactLinearXp) AnvilXpUtil.calculateMinimumLevelForXp(xpCost) else AnvilXpUtil.calculateLevelForXp(xpCost) - val result = DependencyManager.tryTreatAnvilResult(resultItem, AnvilUseType.CUSTOM_CRAFT, cost) + 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(inventory, player, first, second, unitRepairAmount) + 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, @@ -293,7 +300,7 @@ object AnvilMergeLogic { return UnitRepairResult.EMPTY } - val result = DependencyManager.tryTreatAnvilResult(resultItem, AnvilUseType.UNIT_REPAIR, cost) + val result = DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.UNIT_REPAIR, cost) return UnitRepairResult(result, cost, repairAmount) } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt index 4ad6c9b..0da6b30 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt @@ -12,12 +12,13 @@ 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.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.CATreatAnvilResultEvent +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 @@ -234,23 +235,24 @@ object DependencyManager { // 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: AnvilXpUtil.AnvilCost ): ItemStack? { - //TODO - /*val treatEvent = CATreatAnvilResultEvent(event, useType, result, cost) + val treatEvent = CATreatAnvilResult2Event(view, inventory, useType, result, cost) try { unsafeTryTreatAnvilResult(treatEvent) return treatEvent.result } catch (e: Exception) { - logExceptionAndClear(event.view.player, event.inventory, e) + logExceptionAndClear(player, inventory, e) return null - }*/ - return result + } } - private fun unsafeTryTreatAnvilResult(event: CATreatAnvilResultEvent) { + private fun unsafeTryTreatAnvilResult(event: CATreatAnvilResult2Event) { Bukkit.getPluginManager().callEvent(event) excellentEnchantsCompatibility?.treatAnvilResult(event) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt index 54ea2f4..39bfad7 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt @@ -8,7 +8,7 @@ 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.CATreatAnvilResultEvent +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 @@ -218,14 +218,17 @@ class ExcellentEnchantsDependency { return handleRechargeMethod.invoke(this.usedAnvilListener, event, first, second) as Boolean } - fun treatAnvilResult(event: CATreatAnvilResultEvent) { + fun treatAnvilResult(event: CATreatAnvilResult2Event) { val result = event.result if (result == null) return - val first: ItemStack = treatInput(event.event.inventory.getItem(0)) - val second: ItemStack = treatInput(event.event.inventory.getItem(1)) + val first: ItemStack = treatInput(event.leftItem) + val second: ItemStack = treatInput(event.rightItem) - handleCombineMethod.invoke(this.usedAnvilListener, event.event, first, second, result) + val fakeEvent = PrepareAnvilEvent(event.view, result) + handleCombineMethod.invoke(this.usedAnvilListener, fakeEvent, first, second, result) + + event.result = fakeEvent.result } fun testAnvilResult(event: InventoryClickEvent): Any { diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt index a232046..775a685 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -53,6 +53,7 @@ class AnvilResultListener : Listener { 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 @@ -74,7 +75,7 @@ class AnvilResultListener : Listener { } // Test custom recipe - val customRecipeResult = AnvilMergeLogic.testCustomRecipe(player, leftItem, rightItem) + val customRecipeResult = AnvilMergeLogic.testCustomRecipe(view, inventory, player, leftItem, rightItem) if (!customRecipeResult.isEmpty()) { onCustomCraft( event, player, inventory, @@ -90,7 +91,7 @@ class AnvilResultListener : Listener { // Rename if (rightItem == null) { - val result = AnvilMergeLogic.doRenaming(inventory, player, leftItem) + val result = AnvilMergeLogic.doRenaming(view, inventory, player, leftItem) if (result.isEmpty()) return extractAnvilResult( @@ -105,7 +106,7 @@ class AnvilResultListener : Listener { // Merge val canMerge = leftItem.canMergeWith(rightItem) if (canMerge) { - val result = AnvilMergeLogic.doMerge(inventory, player, leftItem, rightItem) + val result = AnvilMergeLogic.doMerge(view, inventory, player, leftItem, rightItem) extractAnvilResult( event, player, inventory, @@ -118,7 +119,7 @@ class AnvilResultListener : Listener { // Unit repair val unitRepairResult = AnvilMergeLogic.testUnitRepair( - inventory, player, + view, inventory, player, leftItem, rightItem ) if (unitRepairResult.isEmpty()) { @@ -481,6 +482,8 @@ class AnvilResultListener : Listener { ) { val paperMeta = rightItem.itemMeta ?: return + + val paperCopy: ItemStack? if (LoreEditType.APPEND_PAPER.doConsume) { paperCopy = null diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index 21438ae..b2f6785 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -11,6 +11,7 @@ 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 @@ -91,11 +92,12 @@ class PrepareAnvilListener : Listener { if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return - val result = getResult(inventory, player, first, second) + 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 @@ -104,7 +106,7 @@ class PrepareAnvilListener : Listener { return AnvilResult.EMPTY // Test custom recipe - var result: AnvilResult = testCustomRecipe(player, first, second) + var result: AnvilResult = testCustomRecipe(view, inventory, player, first, second) if (!result.isEmpty()) return result @@ -112,14 +114,14 @@ class PrepareAnvilListener : Listener { val shouldTryRename = second.isAir CustomAnvil.verboseLog("checking air in main logic: $shouldTryRename") if (shouldTryRename) - return doRenaming(inventory, player, first) + return doRenaming(view, inventory, player, first) // Test for merge if (first.canMergeWith(second!!)) - return doMerge(inventory, player, first, second) + return doMerge(view, inventory, player, first, second) // Test for unit repair - result = testUnitRepair(inventory, player, first, second) + result = testUnitRepair(view, inventory, player, first, second) if (!result.isEmpty()) return result From 49b0054ecaec04170230f4d8bbae589171c79ec2 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Wed, 10 Jun 2026 14:59:28 +0200 Subject: [PATCH 140/166] move anvil cost to its own class --- .../listener/CATreatAnvilResult2Event.java | 8 +-- .../listener/CATreatAnvilResultEvent.java | 6 +- .../xyz/alexcrea/cuanvil/anvil/AnvilCost.kt | 55 +++++++++++++++++++ .../alexcrea/cuanvil/anvil/AnvilMergeLogic.kt | 14 +++-- .../cuanvil/dependency/DependencyManager.kt | 4 +- .../plugins/DisenchantmentDependency.kt | 2 +- .../dependency/plugins/HavenBagsDependency.kt | 2 +- .../cuanvil/listener/AnvilResultListener.kt | 4 +- .../cuanvil/listener/PrepareAnvilListener.kt | 2 +- .../cuanvil/util/anvil/AnvilLoreEditUtil.kt | 40 ++++++++------ .../cuanvil/util/anvil/AnvilXpUtil.kt | 54 +----------------- 11 files changed, 101 insertions(+), 90 deletions(-) create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilCost.kt 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 index aa8f8e1..30c5380 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResult2Event.java +++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResult2Event.java @@ -8,8 +8,8 @@ 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; -import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil; /** * Called after custom anvil processed the click on the result on the anvil inventory. @@ -48,7 +48,7 @@ public class CATreatAnvilResult2Event extends Event { @Nullable private ItemStack result; - private final AnvilXpUtil.AnvilCost cost; + private final AnvilCost cost; @ApiStatus.Internal public CATreatAnvilResult2Event( @@ -56,7 +56,7 @@ public class CATreatAnvilResult2Event extends Event { Inventory inv, AnvilUseType useType, @Nullable ItemStack result, - AnvilXpUtil.AnvilCost cost) { + AnvilCost cost) { this.view = view; this.useType = useType; @@ -190,7 +190,7 @@ public class CATreatAnvilResult2Event extends Event { * * @return the current anvil cost */ - public AnvilXpUtil.AnvilCost getCost() { + 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 index 6e6358d..80965b5 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResultEvent.java +++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResultEvent.java @@ -6,8 +6,8 @@ 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; -import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil.AnvilCost; /** * Called after custom anvil processed the click on the result on the anvil inventory. @@ -108,8 +108,8 @@ public class CATreatAnvilResultEvent extends Event { *

  • Item rename
  • * * - * @deprecated use #{@link #getCost()} instead * @return The current cost. + * @deprecated use #{@link #getCost()} instead */ @Deprecated(forRemoval = true, since = "1.17.0") public int getLevelCost() { @@ -130,8 +130,8 @@ public class CATreatAnvilResultEvent extends Event { *
  • Item rename
  • * * - * @deprecated use #{@link #getCost()} and set value on this instead * @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) { 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..f8ff89c --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilCost.kt @@ -0,0 +1,55 @@ +package xyz.alexcrea.cuanvil.anvil + +import java.math.BigDecimal +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 + } + + 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 index 67b9b7d..6b106fc 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilMergeLogic.kt @@ -29,8 +29,6 @@ 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.anvil.AnvilXpUtil.AnvilCost -import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil.CustomCraftCost import xyz.alexcrea.cuanvil.util.config.LoreEditType import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil @@ -115,7 +113,8 @@ object AnvilMergeLogic { } cost.workPenalty = AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.RENAME_ONLY) - val result = DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.RENAME_ONLY, cost) + val result = + DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.RENAME_ONLY, cost) return AnvilResult(result, cost) } @@ -220,7 +219,8 @@ object AnvilMergeLogic { // Calculate rename cost cost.rename = handleRename(resultItem, inventory, player) - val result = DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.MERGE, cost) + val result = + DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.MERGE, cost) return AnvilResult(result, cost) } @@ -261,7 +261,8 @@ object AnvilMergeLogic { cost.recipe = if (recipe.removeExactLinearXp) AnvilXpUtil.calculateMinimumLevelForXp(xpCost) else AnvilXpUtil.calculateLevelForXp(xpCost) - val result = DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.CUSTOM_CRAFT, cost) + val result = + DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.CUSTOM_CRAFT, cost) return CustomCraftResult(result, cost, amount, recipe) } @@ -300,7 +301,8 @@ object AnvilMergeLogic { return UnitRepairResult.EMPTY } - val result = DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.UNIT_REPAIR, cost) + val result = + DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.UNIT_REPAIR, cost) return UnitRepairResult(result, cost, repairAmount) } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt index 0da6b30..cb24eaa 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt @@ -14,6 +14,7 @@ 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 @@ -33,7 +34,6 @@ 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 xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil import java.util.logging.Level object DependencyManager { @@ -240,7 +240,7 @@ object DependencyManager { player: HumanEntity, result: ItemStack, useType: AnvilUseType, - cost: AnvilXpUtil.AnvilCost + cost: AnvilCost ): ItemStack? { val treatEvent = CATreatAnvilResult2Event(view, inventory, useType, result, cost) try { diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt index 32ca99d..690b384 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/DisenchantmentDependency.kt @@ -14,10 +14,10 @@ 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 xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil.AnvilCost import java.util.logging.Level import kotlin.reflect.KClass diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt index 62d9e4e..6f30497 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/HavenBagsDependency.kt @@ -9,9 +9,9 @@ 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 -import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil.AnvilCost class HavenBagsDependency { diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt index 775a685..feee833 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -15,6 +15,7 @@ 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 @@ -30,10 +31,8 @@ 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.anvil.AnvilXpUtil.AnvilCost import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil import xyz.alexcrea.cuanvil.util.config.LoreEditType -import java.math.BigDecimal import java.util.* import java.util.concurrent.atomic.AtomicReference import kotlin.math.min @@ -483,7 +482,6 @@ class AnvilResultListener : Listener { val paperMeta = rightItem.itemMeta ?: return - val paperCopy: ItemStack? if (LoreEditType.APPEND_PAPER.doConsume) { paperCopy = null diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt index b2f6785..0217983 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -15,6 +15,7 @@ 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 @@ -24,7 +25,6 @@ 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.anvil.AnvilXpUtil.AnvilCost import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil /** diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilLoreEditUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilLoreEditUtil.kt index e9bb633..a021b46 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilLoreEditUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilLoreEditUtil.kt @@ -5,12 +5,12 @@ 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.anvil.AnvilXpUtil.AnvilCost import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil import xyz.alexcrea.cuanvil.util.config.LoreEditType import java.util.* @@ -44,8 +44,10 @@ object AnvilLoreEditUtil { 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) + val colorCost = colorLines( + player, LoreEditType.APPEND_BOOK, + lines, outLines + ) lore.addAll(outLines) @@ -81,7 +83,7 @@ object AnvilLoreEditUtil { // Handle xp cost.lore = uncolorCost - cost.lore+= currentLore.size * LoreEditType.REMOVE_BOOK.perLineCost + cost.lore += currentLore.size * LoreEditType.REMOVE_BOOK.perLineCost baseEditLoreXpCost(cost, first, result, LoreEditType.REMOVE_BOOK) return result @@ -120,7 +122,7 @@ object AnvilLoreEditUtil { 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 type = if (isAppend) LoreEditType.APPEND_BOOK else LoreEditType.REMOVE_BOOK val meta = second.itemMeta as BookMeta val cost = AnvilCost() @@ -166,9 +168,11 @@ object AnvilLoreEditUtil { // A bit overdone to color 1 line but hey val outList = ArrayList(1) - val colorCost = colorLines(player, LoreEditType.APPEND_PAPER, + val colorCost = colorLines( + player, LoreEditType.APPEND_PAPER, Collections.singletonList(second.itemMeta!!.displayName), - outList) + outList + ) val line = outList[0] if (appendEnd) @@ -232,7 +236,7 @@ object AnvilLoreEditUtil { second: ItemStack ): LoreEditResult { val isAppend = paperLoreEditIsAppend(first, second) ?: return LoreEditResult.EMPTY - val type = if(isAppend) LoreEditType.APPEND_BOOK else LoreEditType.REMOVE_BOOK + val type = if (isAppend) LoreEditType.APPEND_BOOK else LoreEditType.REMOVE_BOOK val cost = AnvilCost() val item = if (isAppend) @@ -248,18 +252,20 @@ object AnvilLoreEditUtil { result: ItemStack, editType: LoreEditType ) { - cost.lore+= editType.fixedCost + 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, + return AnvilColorUtil.calculatePermissions( + player, false, editType.allowColorCode, editType.allowHexColor, editType.allowMinimessage, - AnvilColorUtil.ColorUseType.LORE_EDIT) + AnvilColorUtil.ColorUseType.LORE_EDIT + ) } private fun colorLine(line: String, permission: AnvilColorUtil.ColorPermissions): Component? { @@ -269,8 +275,10 @@ object AnvilLoreEditUtil { ) } - private fun colorLines(player: Permissible, editType: LoreEditType, - lines: List, outLines: MutableList): Int { + private fun colorLines( + player: Permissible, editType: LoreEditType, + lines: List, outLines: MutableList + ): Int { val permission = colorPermission(player, editType) val colorCost = editType.useColorCost @@ -297,7 +305,7 @@ object AnvilLoreEditUtil { // Now handle color of each lines var hasUndidColor = false for ((index, line) in lines.withIndex()) { - if(line == null){ + if (line == null) { lines[index] = null continue } @@ -312,7 +320,7 @@ object AnvilLoreEditUtil { hasUndidColor = true result = clearedLine } else { - result = MiniMessageUtil.plain_text_mm.serialize(line) + result = MiniMessageUtil.plain_text_mm.serialize(line) } lines[index] = MiniMessageUtil.plain_text_mm.deserialize(result) @@ -341,7 +349,7 @@ object AnvilLoreEditUtil { var hasUndidColor = false val result: String - if(clearedLine != null){ + if (clearedLine != null) { hasUndidColor = true result = clearedLine } else { diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt index 8c39a0a..f6ee12f 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt @@ -14,6 +14,7 @@ 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 @@ -21,65 +22,12 @@ 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 -import java.math.BigDecimal import kotlin.math.min -import io.delilaheve.util.ConfigOptions.getMonetaryMultiplier as moneyMultiplier object AnvilXpUtil { const val EXCLUSIVE_PENALTY_PREFIX = "repair_cost" - 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 - } - - 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")) - } - - } - /** * Display the required cost (either as xp or as ) */ From 2f9d25bfe9950526fcade3883392192cdbf928a4 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Wed, 10 Jun 2026 15:18:31 +0200 Subject: [PATCH 141/166] fix fake prepare anvil on modern versions --- .../cuanvil/util/ModernPrepareAnvilCreator.kt | 12 ++++++++++++ .../plugins/ExcellentEnchantsDependency.kt | 12 ++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/ModernPrepareAnvilCreator.kt diff --git a/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/ModernPrepareAnvilCreator.kt b/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/ModernPrepareAnvilCreator.kt new file mode 100644 index 0000000..0c51e0a --- /dev/null +++ b/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/ModernPrepareAnvilCreator.kt @@ -0,0 +1,12 @@ +package xyz.alexcrea.cuanvil.util + +import org.bukkit.event.inventory.PrepareAnvilEvent +import org.bukkit.inventory.InventoryView +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.view.AnvilView + +object ModernPrepareAnvilCreator { + fun createPrepareAnvil(view: InventoryView, item: ItemStack?): PrepareAnvilEvent { + return PrepareAnvilEvent(view as AnvilView, 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 index 39bfad7..c2d3019 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt @@ -13,6 +13,7 @@ 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 xyz.alexcrea.cuanvil.util.ModernPrepareAnvilCreator import java.lang.reflect.Method import su.nightexpress.excellentenchants.api.EnchantRegistry as V5EnchantRegistry import su.nightexpress.excellentenchants.enchantment.impl.universal.CurseOfFragilityEnchant as LegacyCurseOfFragilityEnchant @@ -219,13 +220,16 @@ class ExcellentEnchantsDependency { } fun treatAnvilResult(event: CATreatAnvilResult2Event) { - val result = event.result - if (result == null) return + val result = event.result ?: return val first: ItemStack = treatInput(event.leftItem) val second: ItemStack = treatInput(event.rightItem) - - val fakeEvent = PrepareAnvilEvent(event.view, result) + val fakeEvent: PrepareAnvilEvent = try { + //TODO remove this on legacy removal + PrepareAnvilEvent(event.view, result) + } catch (_: NoSuchMethodError) { + ModernPrepareAnvilCreator.createPrepareAnvil(event.view, result) + } handleCombineMethod.invoke(this.usedAnvilListener, fakeEvent, first, second, result) event.result = fakeEvent.result From d91576b0deb927c5c7b123f2d7f50a3c69a0d24e Mon Sep 17 00:00:00 2001 From: alexcrea Date: Wed, 10 Jun 2026 15:30:25 +0200 Subject: [PATCH 142/166] update faststat [skip ci] --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 71945d5..bf81368 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -55,7 +55,7 @@ dependencies { compileOnly("org.spigotmc:spigot-api:1.18-R0.1-SNAPSHOT") // fast stats - implementation("dev.faststats.metrics:bukkit:0.24.0") + implementation("dev.faststats.metrics:bukkit:0.25.1") // minimessage implementation("net.kyori:adventure-text-minimessage:4.25.0") From d82bd9b22c24861dbf78e1dc093ca61840483b47 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Wed, 10 Jun 2026 17:07:52 +0200 Subject: [PATCH 143/166] do not packet manager on test --- .../cuanvil/dependency/DependencyManager.kt | 8 +++----- .../dependency/gui/GenericExternGuiTester.kt | 8 ++++++-- .../cuanvil/dependency/gui/GuiTesterSelector.kt | 15 --------------- .../dependency/packet/PacketManagerSelector.kt | 4 ++++ 4 files changed, 13 insertions(+), 22 deletions(-) delete mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GuiTesterSelector.kt diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt index cb24eaa..b730a0e 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt @@ -23,7 +23,6 @@ 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.gui.GuiTesterSelector import xyz.alexcrea.cuanvil.dependency.packet.PacketManager import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerSelector import xyz.alexcrea.cuanvil.dependency.plugins.* @@ -40,7 +39,7 @@ object DependencyManager { lateinit var scheduler: TaskScheduler lateinit var packetManager: PacketManager - var externGuiTester: GenericExternGuiTester? = null + var externGuiTester: GenericExternGuiTester = GenericExternGuiTester() var enchantmentSquaredCompatibility: EnchantmentSquaredDependency? = null var ecoEnchantCompatibility: EcoEnchantDependency? = null @@ -68,7 +67,6 @@ object DependencyManager { // Packet Manager val forceProtocolib = ConfigHolder.DEFAULT_CONFIG.config.getBoolean("force_protocolib", false) packetManager = PacketManagerSelector.selectPacketManager(forceProtocolib) - externGuiTester = GuiTesterSelector.selectGuiTester // Enchantment Squared dependency if (pluginManager.isPluginEnabled("EnchantsSquared")) { @@ -192,7 +190,7 @@ object DependencyManager { var bypass = bypassEvent.isCancelled // Test if the inventory is a gui(version specific) - if (!bypass && (externGuiTester?.testIfGui(event.view) == true)) bypass = true + 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 @@ -289,7 +287,7 @@ object DependencyManager { } // Test if the inventory is a gui(version specific) - if (!bypass && (externGuiTester?.testIfGui(event.view) == true)) bypass = true + 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 diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GenericExternGuiTester.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GenericExternGuiTester.kt index 0e430ef..4046f4a 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GenericExternGuiTester.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GenericExternGuiTester.kt @@ -36,6 +36,11 @@ class GenericExternGuiTester { getHandleMethod = clazz.getMethod(HANDLE_METHOD_NAME) } + fun isInTest(): Boolean { + if(!testExist) testClassExist() + return inTesting + } + fun testClassExist() { testExist = true @@ -61,8 +66,7 @@ class GenericExternGuiTester { // Try if were in another plugin anvil inventory fun testIfGui(inventory: InventoryView): Boolean { // In case we are in a test environment - if(!testExist) testClassExist() - if(inTesting) return false + if(isInTest()) return false val clazz = getContainerClass(inventory) ?: return false diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GuiTesterSelector.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GuiTesterSelector.kt deleted file mode 100644 index e445d8d..0000000 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GuiTesterSelector.kt +++ /dev/null @@ -1,15 +0,0 @@ -package xyz.alexcrea.cuanvil.dependency.gui - -import xyz.alexcrea.cuanvil.update.UpdateUtils - -object GuiTesterSelector { - - val selectGuiTester: GenericExternGuiTester? - get() { - val versionParts = UpdateUtils.currentMinecraftVersionArray() - if (versionParts[0] != 1) return null - - return GenericExternGuiTester() - } - -} \ 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 index 4c3d680..6c18a75 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerSelector.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerSelector.kt @@ -1,6 +1,7 @@ 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 @@ -11,6 +12,9 @@ object PacketManagerSelector { fun selectPacketManager(forceProtocolib: Boolean): PacketManager { // Try to find version + if(DependencyManager.externGuiTester.isInTest()) + return NoPacketManager() + return if (forceProtocolib) protocolibIfPresent else { From b0f32fdba231b03610745da9c3375b43f176f05a Mon Sep 17 00:00:00 2001 From: alexcrea Date: Wed, 10 Jun 2026 17:18:43 +0200 Subject: [PATCH 144/166] do not load metrics in test --- .../kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt | 3 +++ src/test/java/io/delilaheve/CustomAnvilTests.java | 12 ++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt index 81ac870..1763db5 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/MetricsUtil.kt @@ -7,6 +7,7 @@ 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 { @@ -17,6 +18,8 @@ object MetricsUtil { 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) diff --git a/src/test/java/io/delilaheve/CustomAnvilTests.java b/src/test/java/io/delilaheve/CustomAnvilTests.java index 45ecb7c..12e1ec1 100644 --- a/src/test/java/io/delilaheve/CustomAnvilTests.java +++ b/src/test/java/io/delilaheve/CustomAnvilTests.java @@ -8,11 +8,15 @@ public class CustomAnvilTests extends DefaultCustomAnvilTest { @Test public void simpleInitTest() { - Assertions.assertNotNull(server); - Assertions.assertNotNull(plugin); + try { + Assertions.assertNotNull(server); + Assertions.assertNotNull(plugin); - // Test shutdown - plugin.onDisable(); + // Test shutdown + plugin.onDisable(); + } catch (Exception e) { + Assertions.fail(e); + } } } From 12ec4e1f54822f254e7770a1df91ffca4a6a202f Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sat, 13 Jun 2026 15:14:33 +0200 Subject: [PATCH 145/166] update faststats fix a potential plugin issue with disabling faststat in a certain way and fix potential submission on first run --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index fedeb77..6d2047e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } group = "xyz.alexcrea" -version = "1.17.0" +version = "1.17.1" val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null val isPreRelease = System.getenv("IS_GITHUB_PRERELEASE") == "true" @@ -58,7 +58,7 @@ dependencies { compileOnly("org.spigotmc:spigot-api:1.18-R0.1-SNAPSHOT") // fast stats - implementation("dev.faststats.metrics:bukkit:0.25.1") + implementation("dev.faststats.metrics:bukkit:0.26.1") // minimessage implementation("net.kyori:adventure-text-minimessage:4.25.0") From 357832268632dde8284f54108d0059fde2881c97 Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:22:39 +0200 Subject: [PATCH 146/166] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22d9da1..dab8ab6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Custom Anvil **Custom Anvil** is a plugin that allows server administrators to customize every aspect of the anvil's mechanics. -It is expected to work on 1.18 to 1.21.7 minecraft servers running spigot or paper. +It is expected to work on 1.18 to 26.1.2 minecraft servers running spigot or paper. (the plugin support of 1.16.5 to 1.17.1 is experimental and may encounter issues) ### Download Locations: From 593527241a82edbb39f1c78bdcaf7b1333c103d0 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 16 Jun 2026 02:57:41 +0200 Subject: [PATCH 147/166] fix bugged unit repair & version bump --- build.gradle.kts | 2 +- .../kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 6d2047e..7b449fc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } group = "xyz.alexcrea" -version = "1.17.1" +version = "1.17.2" val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null val isPreRelease = System.getenv("IS_GITHUB_PRERELEASE") == "true" diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt index feee833..c5de6a8 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -121,7 +121,7 @@ class AnvilResultListener : Listener { view, inventory, player, leftItem, rightItem ) - if (unitRepairResult.isEmpty()) { + if (!unitRepairResult.isEmpty()) { onUnitRepairExtract( rightItem, event, player, inventory, unitRepairResult From 151666fd21a59fb3d0c8b87e201a14f05bcb9e29 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 16 Jun 2026 03:09:41 +0200 Subject: [PATCH 148/166] better excellent enchant fake event [skip ci] --- .../cuanvil/util/ModernPrepareAnvilCreator.kt | 12 ------------ .../plugins/ExcellentEnchantsDependency.kt | 12 +++++------- 2 files changed, 5 insertions(+), 19 deletions(-) delete mode 100644 nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/ModernPrepareAnvilCreator.kt diff --git a/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/ModernPrepareAnvilCreator.kt b/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/ModernPrepareAnvilCreator.kt deleted file mode 100644 index 0c51e0a..0000000 --- a/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/util/ModernPrepareAnvilCreator.kt +++ /dev/null @@ -1,12 +0,0 @@ -package xyz.alexcrea.cuanvil.util - -import org.bukkit.event.inventory.PrepareAnvilEvent -import org.bukkit.inventory.InventoryView -import org.bukkit.inventory.ItemStack -import org.bukkit.inventory.view.AnvilView - -object ModernPrepareAnvilCreator { - fun createPrepareAnvil(view: InventoryView, item: ItemStack?): PrepareAnvilEvent { - return PrepareAnvilEvent(view as AnvilView, 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 index c2d3019..816a4df 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/plugins/ExcellentEnchantsDependency.kt @@ -13,7 +13,7 @@ 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 xyz.alexcrea.cuanvil.util.ModernPrepareAnvilCreator +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 @@ -118,6 +118,8 @@ class ExcellentEnchantsDependency { 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 @@ -224,12 +226,8 @@ class ExcellentEnchantsDependency { val first: ItemStack = treatInput(event.leftItem) val second: ItemStack = treatInput(event.rightItem) - val fakeEvent: PrepareAnvilEvent = try { - //TODO remove this on legacy removal - PrepareAnvilEvent(event.view, result) - } catch (_: NoSuchMethodError) { - ModernPrepareAnvilCreator.createPrepareAnvil(event.view, result) - } + val fakeEvent = prepareAnvilConstructor.newInstance(event.view, result) + handleCombineMethod.invoke(this.usedAnvilListener, fakeEvent, first, second, result) event.result = fakeEvent.result From 29e08fe29bb81173916935bdc4987c6a23e2aa8b Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 16 Jun 2026 03:17:53 +0200 Subject: [PATCH 149/166] fix double space issue [ci skip] --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 8c4a93c..66c1ad1 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -157,7 +157,7 @@ jobs: 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) + # 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 }} From fc33b6fbd5c0386d88fb2a600064205d4eaf9008 Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Tue, 16 Jun 2026 03:27:10 +0200 Subject: [PATCH 150/166] Update README.md --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dab8ab6..5e70842 100644 --- a/README.md +++ b/README.md @@ -63,13 +63,10 @@ See the [Compatibility list](https://github.com/alexcrea/CustomAnvil/blob/v1.x.x 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. -Minecraft version 1.18 to latest marked as supported do not need any ProtocoLib dependency. \ +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. -Other version need ProtocoLib enabled on your server for this feature. \ -You can also wait for an update of the plugin to support a newer version. - -Please note that 1.16.5 to 1.17.1 are not officially supported. Run at your own risk. +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) \ From c064e4b1e1f5e628f6e903966410be38f3f3371c Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Tue, 16 Jun 2026 03:32:46 +0200 Subject: [PATCH 151/166] forgot \ [skip ci] --- defaultconfigs/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/defaultconfigs/README.md b/defaultconfigs/README.md index a413b66..9e85e52 100644 --- a/defaultconfigs/README.md +++ b/defaultconfigs/README.md @@ -1,5 +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) \ No newline at end of file +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) \ From b92b762551ed3e15266b0fb0b1a3ad44cbc6c36a Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Tue, 16 Jun 2026 03:33:03 +0200 Subject: [PATCH 152/166] overdid \ [skip ci] --- defaultconfigs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/defaultconfigs/README.md b/defaultconfigs/README.md index 9e85e52..daa2088 100644 --- a/defaultconfigs/README.md +++ b/defaultconfigs/README.md @@ -2,4 +2,4 @@ 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) \ +From 1.21.11 use [1.21.11 configurations](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs/1.21.11) From 96754fd2606407bc06ff2710434fdd27b007b4bb Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Tue, 16 Jun 2026 03:48:47 +0200 Subject: [PATCH 153/166] better readme [skip ci] --- README.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5e70842..3c597b8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # Custom Anvil **Custom Anvil** is a plugin that allows server administrators to customize every aspect of the anvil's mechanics. -It is expected to work on 1.18 to 26.1.2 minecraft servers running spigot or paper. -(the plugin support of 1.16.5 to 1.17.1 is experimental and may encounter issues) ### Download Locations: @@ -16,7 +14,7 @@ the plugin can be downloaded on - Vanilla like default configuration. - Custom enchantment level limit. - Custom anvil recipes. -- Custom enchant restrictions (allow unsafe enchantment only for a group of item or create new restriction). +- 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. @@ -25,8 +23,12 @@ the plugin can be downloaded on - 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 others ! --- ### 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 # Generic and bypass permissions ca.affected: Player with this permission will be affected by the plugin @@ -44,17 +46,22 @@ ca.config.edit: Allow administrator to edit the plugin's config in game # ----------------------------------------------------------------------------- # 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) (only legacy compatible at the time) +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) ``` ### Commands -run `customanvil help` to get information about available commands (need permissions to use them) +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) @@ -97,3 +104,6 @@ Credits and thanks can be seen [here](https://github.com/alexcrea/CustomAnvil/bl ### 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)​ From 98d359f59fe0085f5a72b2fbabfe8c438c6cc96a Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Tue, 16 Jun 2026 03:49:16 +0200 Subject: [PATCH 154/166] markdown issue [skip ci] --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c597b8..afe6017 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ the plugin can be downloaded on - (Experimental) Folia support (gui do not work) - (Experimental) Dialog rename (allows longer rename) - (Experimental) Anvil with monetary cost (using vault) (require dialog rename) -And others ! + +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)`) From 4b5133c8720ed4ea344f75714aadc3301da6ab9b Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Tue, 16 Jun 2026 03:49:32 +0200 Subject: [PATCH 155/166] =?UTF-8?q?markdown=20issue=C2=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index afe6017..9d9e678 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ the plugin can be downloaded on - (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)`) From cff94a2c5a6eb23ceef009fb1fb88fdc2416d0d1 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Tue, 16 Jun 2026 04:04:49 +0200 Subject: [PATCH 156/166] better thanks and put names in compatibility note [ci skip] --- COMPATIBILITY.md | 28 ++++++++++++++-------------- CREDITS.md | 21 ++++++++------------- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 66ac561..916a089 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -6,54 +6,54 @@ This is cannot be fixed on geyser or my side. Here is various plugins that had issues with CustomAnvil where efforts was made for compatibility and should be working right: -some of them are cool I recommend checking them out ! +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/): +- [ExcellentEnchants](https://www.spigotmc.org/resources/excellentenchants-%E2%AD%90-75-vanilla-like-enchantments.61693/) by NightExpress: Use ExcellentEnchants item type -- [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/): +- [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/): +- [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) +- [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/) +- [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/) +- [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/) +- [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/) +- [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) +- [AxPlayerWarp](https://modrinth.com/project/QDJHDKvi) by ArtillexStudios For its anvil inventory usage -- [ToolsStats](https://modrinth.com/project/oBZj9E15) +- [ToolsStats](https://modrinth.com/project/oBZj9E15) by Valorless For token application using anvil ### Known Partially Incompatible -- [UberEnchant](https://modrinth.com/plugin/uberenchant) +- [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 recomend checking them out +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/) +- [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 diff --git a/CREDITS.md b/CREDITS.md index 528ad15..56409f5 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -5,7 +5,7 @@ Thanks for all the contributors of bukkit, spigot, the paper team and the advent Thanks JetBrain for making IntelliJ ### Dependencies -Here dependencies are used by custom anvil +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 @@ -17,23 +17,18 @@ Here dependencies are used by custom anvil - [ModrinthUpdateChecker](https://github.com/Clickism/ModrinthUpdateChecker) by Clickism and thanks to the modrinth team ### Compatibility -Here is to credits all the author of plugins -It partially repeat the the [Compatibility list](https://github.com/alexcrea/CustomAnvil/blob/v1.x.x/COMPATIBILITY.md) -- Big Thanks for H7KZ for [Disenchantment](https://github.com/H7KZ/Disenchantment) -- [Enchantment²](https://www.spigotmc.org/resources/enchants-squared-the-enchantsplus-rewrite-custom-enchantments-that-act-like-vanilla-ones.86747/) by Athlaeos -- [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 -- [ExcellentEnchants](https://www.spigotmc.org/resources/excellentenchants-%E2%AD%90-75-vanilla-like-enchantments.61693/) by NightExpress -- [HavenBags](https://www.spigotmc.org/resources/havenbags-shulker-like-player-bound-bags-1-17-1-21-4.110420/) by hyperdefined -- [AxPlayerWarp](https://modrinth.com/project/QDJHDKvi) by ArtillexStudios -- [ToolsStats](https://modrinth.com/project/oBZj9E15) by Valorless +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 Microsoft leading me into using a better operating system \ Thanks for all the users trying my plugin for these niche use cases -and for reporting issues and giving ideas ! +, 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 +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 ! * From f82ccfa07e34ff5e2dde0256ef9c9013625e3c12 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Wed, 17 Jun 2026 02:14:09 +0200 Subject: [PATCH 157/166] make Version work with experimental 26.1 paper build --- src/main/java/xyz/alexcrea/cuanvil/update/Version.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/Version.java b/src/main/java/xyz/alexcrea/cuanvil/update/Version.java index 15476f5..a49fbdd 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/update/Version.java +++ b/src/main/java/xyz/alexcrea/cuanvil/update/Version.java @@ -21,7 +21,11 @@ public record Version(int major, int minor, int patch) { int[] versionParts = new int[]{0, 0, 0}; for (int i = 0; i < Math.min(3, partialVersion.length); i++) { - versionParts[i] = Integer.parseInt(partialVersion[i]); + try { + versionParts[i] = Integer.parseInt(partialVersion[i]); + } catch (NumberFormatException e) { + break; + } } return new Version(versionParts[0], versionParts[1], versionParts[2]); } From 9d616d2fd0bb4980d14f1fb85f26df207a7301dc Mon Sep 17 00:00:00 2001 From: alexcrea Date: Wed, 17 Jun 2026 02:14:53 +0200 Subject: [PATCH 158/166] remove use of legacy currentMinecraftVersionArray --- .../gui/config/global/BasicConfigGui.java | 12 +++--------- .../alexcrea/cuanvil/update/UpdateUtils.java | 16 ---------------- .../dependency/MinecraftVersionUtil.kt | 19 +++++++++---------- .../packet/PacketManagerSelector.kt | 6 +++--- 4 files changed, 15 insertions(+), 38 deletions(-) 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 index 97f8e54..51936c7 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/BasicConfigGui.java +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/BasicConfigGui.java @@ -283,15 +283,9 @@ public class BasicConfigGui extends ChestGui implements ValueUpdatableGui { lore.add("§7If the player do not have the required xp level, the action will not be completable."); if(!this.packetManager.getCanSetInstantBuild()){ - if(MinecraftVersionUtil.INSTANCE.isTooNewForSpigot()){ - lore.add(""); - lore.add("§4/!\\§cCaution§4/!\\ §cYou need ProtocoLib installed and working or a paper server."); - lore.add("§cCurrently ProtocoLib is not detected."); - } else { - lore.add(""); - lore.add("§4/!\\§cCaution§4/!\\ §cYou need ProtocoLib installed and working or a newer version of this plugin for this to work."); - lore.add("§cCurrently ProtocoLib is not detected."); - } + 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()]; diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/UpdateUtils.java b/src/main/java/xyz/alexcrea/cuanvil/update/UpdateUtils.java index c13515f..2907fef 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/update/UpdateUtils.java +++ b/src/main/java/xyz/alexcrea/cuanvil/update/UpdateUtils.java @@ -15,22 +15,6 @@ public class UpdateUtils { return Version.fromString(versionString); } - @Deprecated - public static int[] currentMinecraftVersionArray() { - String versionString = Bukkit.getServer().getBukkitVersion().split("-")[0]; - return UpdateUtils.readVersionFromString(versionString); - } - - public static int[] readVersionFromString(String versionString) { - String[] partialVersion = versionString.split("\\."); - int[] versionParts = new int[]{0, 0, 0}; - - for (int i = 0; i < Math.min(3, partialVersion.length); i++) { - versionParts[i] = Integer.parseInt(partialVersion[i]); - } - return versionParts; - } - public static void addToStringList(FileConfiguration config, String path, String... toAdd) { List groups = new ArrayList<>(config.getStringList(path)); groups.addAll(Arrays.asList(toAdd)); diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/MinecraftVersionUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/MinecraftVersionUtil.kt index d981f81..cbd2209 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/MinecraftVersionUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/MinecraftVersionUtil.kt @@ -6,29 +6,29 @@ object MinecraftVersionUtil { val craftbukkitVersion: String? get() { - val versionParts = UpdateUtils.currentMinecraftVersionArray() - if (versionParts[0] != 1) return null + val version = UpdateUtils.currentMinecraftVersion() + if (version.major != 1) return null - return when (versionParts[1]) { - 17 -> when (versionParts[2]) { + return when (version.minor) { + 17 -> when (version.patch) { 0, 1 -> "1_17R1" else -> null } - 18 -> when (versionParts[2]) { + 18 -> when (version.patch) { 0, 1 -> "1_18R1" 2 -> "1_18R2" else -> null } - 19 -> when (versionParts[2]) { + 19 -> when (version.patch) { 0, 1, 2 -> "1_19R1" 3 -> "1_19R2" 4 -> "1_19R3" else -> null } - 20 -> when (versionParts[2]) { + 20 -> when (version.patch) { 0, 1 -> "1_20R1" 2 -> "1_20R2" 3, 4 -> "1_20R3" @@ -36,7 +36,7 @@ object MinecraftVersionUtil { else -> null } - 21 -> when (versionParts[2]) { + 21 -> when (version.patch) { 0, 1 -> "1_21R1" 2, 3 -> "1_21R2" 4 -> "1_21R3" @@ -52,8 +52,7 @@ object MinecraftVersionUtil { } val isTooNewForSpigot: Boolean get() { - val versionParts = UpdateUtils.currentMinecraftVersionArray() - return versionParts[0] != 1 + return UpdateUtils.currentMinecraftVersion().major != 1 } } \ 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 index 6c18a75..d74ef08 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerSelector.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/PacketManagerSelector.kt @@ -38,8 +38,8 @@ object PacketManagerSelector { // Reobfuscated packet manager for spigot or paper as it remap private val reobfPacketManager: PacketManagerBase? get() { - val versionParts = UpdateUtils.currentMinecraftVersionArray() - if (versionParts[0] != 1) return null + val versionParts = UpdateUtils.currentMinecraftVersion() + if (versionParts.major != 1) return null try { val clazz = Class.forName("xyz.alexcrea.cuanvil.dependency.packet.versions." + @@ -47,7 +47,7 @@ object PacketManagerSelector { val manager = clazz.getConstructor().newInstance() return manager as PacketManagerBase - } catch (e: ClassNotFoundException) { + } catch (_: ClassNotFoundException) { return null } } From bc9cbe0b4459b1c00ccf44b2dde570d98f3b92ec Mon Sep 17 00:00:00 2001 From: alexcrea Date: Fri, 19 Jun 2026 20:29:10 +0200 Subject: [PATCH 159/166] fix and simplify xp handling - Fix xp limit not being respected - Fix player xp not being check causing error --- .../xyz/alexcrea/cuanvil/anvil/AnvilCost.kt | 17 +++++++++ .../cuanvil/listener/AnvilResultListener.kt | 12 ++++-- .../cuanvil/util/anvil/AnvilXpUtil.kt | 38 +++++++------------ 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilCost.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilCost.kt index f8ff89c..710687c 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilCost.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/anvil/AnvilCost.kt @@ -1,6 +1,8 @@ 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 { @@ -30,6 +32,21 @@ open class AnvilCost { 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) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt index c5de6a8..e393dbd 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -270,13 +270,19 @@ class AnvilResultListener : Listener { } - private fun tryRemoveCost(player: Player, cost: AnvilCost): Boolean { + 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 { - player.level -= cost.asXpCost() + val xpCost = cost.filteredXpCost() + if (xpCost > AnvilXpUtil.maximumXpCost(result.ignoreXpRules)) return false + if (player.level < xpCost) return false + + player.level -= xpCost } return true @@ -308,7 +314,7 @@ class AnvilResultListener : Listener { // If not creative middle click... if (event.click != ClickType.MIDDLE) { - if (!tryRemoveCost(player, cost)) return false + if (!tryRemoveCost(player, result)) return false // We remove what should be removed if (leftItem != null) leftItem.amount -= leftRemoveCount diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt index f6ee12f..4846f31 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/anvil/AnvilXpUtil.kt @@ -22,7 +22,6 @@ 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 -import kotlin.math.min object AnvilXpUtil { @@ -42,7 +41,15 @@ object AnvilXpUtil { cost.isMonetary = true setAnvilPrice(inventory, view, player, cost) } else - setAnvilInvXp(inventory, view, player, cost.asXpCost(), ignoreRules) + 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 + } } /** @@ -55,28 +62,11 @@ object AnvilXpUtil { anvilCost: Int, ignoreRules: Boolean = false ) { - - // Test repair cost limit - val finalAnvilCost = if ( - !ignoreRules && - !ConfigOptions.doRemoveCostLimit && - ConfigOptions.doCapCost - ) { - min(anvilCost, ConfigOptions.maxAnvilCost) - } else { - anvilCost - } - - val maximumRepairCost = - if (ConfigOptions.doRemoveCostLimit || ignoreRules) { - Int.MAX_VALUE - } else { - ConfigOptions.maxAnvilCost + 1 - } + val maximumRepairCost = maximumXpCost(ignoreRules) // Try first just in case another plugin, or the test need this inventory.maximumRepairCost = maximumRepairCost - inventory.repairCost = finalAnvilCost + 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 @@ -87,15 +77,15 @@ object AnvilXpUtil { ) { // retry after a tick inventory.maximumRepairCost = maximumRepairCost - inventory.repairCost = finalAnvilCost + 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) && - (finalAnvilCost >= 40) && - finalAnvilCost < inventory.maximumRepairCost + (anvilCost >= 40) && + anvilCost < inventory.maximumRepairCost DependencyManager.packetManager.setInstantBuild(player, bypassToExpensive) } From 950bad2168c67d8bdd6dd60360e14161c9879662 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Fri, 19 Jun 2026 20:31:31 +0200 Subject: [PATCH 160/166] versions bump --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7b449fc..bb5a97f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } group = "xyz.alexcrea" -version = "1.17.2" +version = "1.17.3" val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null val isPreRelease = System.getenv("IS_GITHUB_PRERELEASE") == "true" @@ -58,7 +58,7 @@ dependencies { compileOnly("org.spigotmc:spigot-api:1.18-R0.1-SNAPSHOT") // fast stats - implementation("dev.faststats.metrics:bukkit:0.26.1") + implementation("dev.faststats.metrics:bukkit:0.27.0") // minimessage implementation("net.kyori:adventure-text-minimessage:4.25.0") From 106cd53b02fcbb3d32331471ce77d471588d7861 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sat, 20 Jun 2026 03:27:58 +0200 Subject: [PATCH 161/166] mark 26.2.x as supported on modrinth & hangar [ci skip] --- .github/workflows/gradle.yml | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 66c1ad1..a50cacd 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -26,7 +26,7 @@ jobs: contents: write env: - MODRINTH_VERSIONS: '["1.18.x", "1.19.x", "1.20.x", "1.21.x", "26.1.x"]' + 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: diff --git a/gradle.properties b/gradle.properties index 19e22ef..4397d2e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,5 +9,5 @@ kotlin.daemon.jvmargs=-Xmx8G 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.1.2 +paperVersion=1.18-26.2 From 178b372255d510e63f97958ba9d9a9171941832c Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sat, 20 Jun 2026 14:55:31 +0200 Subject: [PATCH 162/166] paper mns use getter/setter no property access [ci skip] --- build.gradle.kts | 2 +- .../cuanvil/dependency/packet/versions/PaperPacketManager.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index bb5a97f..8fde3e6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } group = "xyz.alexcrea" -version = "1.17.3" +version = "1.17.4" val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null val isPreRelease = System.getenv("IS_GITHUB_PRERELEASE") == "true" 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 index 7e248e1..ee0c101 100644 --- 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 @@ -24,8 +24,8 @@ class PaperPacketManager : PacketManagerBase(), PacketManager { sendedAbilities.mayfly = playerAbilities.mayfly sendedAbilities.instabuild = instantBuild sendedAbilities.mayBuild = playerAbilities.mayBuild - sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed - sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed + sendedAbilities.setFlyingSpeed(playerAbilities.getFlyingSpeed()) + sendedAbilities.setWalkingSpeed(playerAbilities.getWalkingSpeed()) } val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities) nmsPlayer.connection.send(packet) From 95d3cf3228f1344fb75e8170c705337c7140c541 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sat, 20 Jun 2026 23:43:01 +0200 Subject: [PATCH 163/166] update nightexpress --- impl/ExcellentEnchant5_4/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/ExcellentEnchant5_4/build.gradle.kts b/impl/ExcellentEnchant5_4/build.gradle.kts index 87694be..f8c103d 100644 --- a/impl/ExcellentEnchant5_4/build.gradle.kts +++ b/impl/ExcellentEnchant5_4/build.gradle.kts @@ -13,5 +13,5 @@ repositories { dependencies { // Excellent Enchant compileOnly("su.nightexpress.excellentenchants:Core:5.4.1") - compileOnly("su.nightexpress.nightcore:main:2.14.1") + compileOnly("su.nightexpress.nightcore:main:2.16.2") } \ No newline at end of file From 37e8ca7da9b0af7546736f147e7ba19bbe0cbe37 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Sat, 20 Jun 2026 23:49:03 +0200 Subject: [PATCH 164/166] update excellentenchants --- impl/ExcellentEnchant5_4/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/ExcellentEnchant5_4/build.gradle.kts b/impl/ExcellentEnchant5_4/build.gradle.kts index f8c103d..9fe598b 100644 --- a/impl/ExcellentEnchant5_4/build.gradle.kts +++ b/impl/ExcellentEnchant5_4/build.gradle.kts @@ -12,6 +12,6 @@ repositories { dependencies { // Excellent Enchant - compileOnly("su.nightexpress.excellentenchants:Core:5.4.1") + compileOnly("su.nightexpress.excellentenchants:Core:5.4.3") compileOnly("su.nightexpress.nightcore:main:2.16.2") } \ No newline at end of file From 7f7f049b7bf74b20228ea21261a587b049858958 Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Mon, 22 Jun 2026 01:01:45 +0200 Subject: [PATCH 165/166] Update COMPATIBILITY.md --- COMPATIBILITY.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 916a089..9d37521 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -13,7 +13,8 @@ These plugins have compatibility handled by custom anvil. seek help on custom an #### Enchantment Plugins - [ExcellentEnchants](https://www.spigotmc.org/resources/excellentenchants-%E2%AD%90-75-vanilla-like-enchantments.61693/) by NightExpress: - Use ExcellentEnchants item type + 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. From eb2e7b3abb3627cbc16dd83ba414c7e4909b6155 Mon Sep 17 00:00:00 2001 From: alexcrea Date: Mon, 22 Jun 2026 03:11:46 +0200 Subject: [PATCH 166/166] allow enchanted book for super enchant --- build.gradle.kts | 2 +- .../cuanvil/enchant/wrapped/CASuperEnchantEnchantment.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 8fde3e6..a5dc7c3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } group = "xyz.alexcrea" -version = "1.17.4" +version = "1.17.5" val isDevBuild = System.getenv("SMALL_COMMIT_HASH") != null val isPreRelease = System.getenv("IS_GITHUB_PRERELEASE") == "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 index f5943be..6039dc8 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CASuperEnchantEnchantment.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CASuperEnchantEnchantment.java @@ -5,6 +5,7 @@ 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; @@ -68,6 +69,8 @@ public class CASuperEnchantEnchantment extends CAEnchantmentBase implements Addi @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()); } }