From 7c283dc7f8407d94724dbc572e31053f6bd44ea9 Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Tue, 15 Oct 2024 06:46:05 +0200 Subject: [PATCH] Splitted main logic to be more readable --- .../io/delilaheve/AnvilEventListener.kt | 580 ------------------ src/main/kotlin/io/delilaheve/CustomAnvil.kt | 7 +- .../dependency/DisenchantmentDependency.kt | 6 +- .../cuanvil/listener/AnvilCloseListener.kt | 23 + .../cuanvil/listener/AnvilResultListener.kt | 292 +++++++++ .../cuanvil/listener/PrepareAnvilListener.kt | 206 +++++++ .../alexcrea/cuanvil/util/AnvilColorUtil.kt | 88 +++ .../alexcrea/cuanvil/util/CustomRecipeUtil.kt | 54 ++ 8 files changed, 672 insertions(+), 584 deletions(-) delete mode 100644 src/main/kotlin/io/delilaheve/AnvilEventListener.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilCloseListener.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilColorUtil.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/util/CustomRecipeUtil.kt diff --git a/src/main/kotlin/io/delilaheve/AnvilEventListener.kt b/src/main/kotlin/io/delilaheve/AnvilEventListener.kt deleted file mode 100644 index bdb1ac9..0000000 --- a/src/main/kotlin/io/delilaheve/AnvilEventListener.kt +++ /dev/null @@ -1,580 +0,0 @@ -package io.delilaheve - -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.GameMode -import org.bukkit.Material -import org.bukkit.entity.HumanEntity -import org.bukkit.entity.Player -import org.bukkit.event.Event -import org.bukkit.event.EventHandler -import org.bukkit.event.EventPriority.HIGHEST -import org.bukkit.event.Listener -import org.bukkit.event.inventory.ClickType -import org.bukkit.event.inventory.InventoryClickEvent -import org.bukkit.event.inventory.InventoryCloseEvent -import org.bukkit.event.inventory.PrepareAnvilEvent -import org.bukkit.inventory.AnvilInventory -import org.bukkit.inventory.InventoryView.Property.REPAIR_COST -import org.bukkit.inventory.ItemStack -import xyz.alexcrea.cuanvil.config.ConfigHolder -import xyz.alexcrea.cuanvil.dependency.DependencyManager -import xyz.alexcrea.cuanvil.dependency.packet.PacketManager -import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe -import xyz.alexcrea.cuanvil.util.AnvilXpUtil.calculatePenalty -import xyz.alexcrea.cuanvil.util.AnvilXpUtil.getRightValues -import xyz.alexcrea.cuanvil.util.AnvilXpUtil.setAnvilInvXp -import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair -import java.util.regex.Matcher -import java.util.regex.Pattern -import kotlin.math.min - - -/** - * Listener for anvil events - */ -class AnvilEventListener(private val packetManager: PacketManager) : Listener { - - companion object { - // Anvil's output slot - const val ANVIL_INPUT_LEFT = 0 - const val ANVIL_INPUT_RIGHT = 1 - const val ANVIL_OUTPUT_SLOT = 2 - - // static slot container - private val NO_SLOT = SlotContainer(SlotType.NO_SLOT, 0) - private val CURSOR_SLOT = SlotContainer(SlotType.CURSOR, 0) - } - - /** - * Event handler logic for when an anvil contains items to be combined - */ - @EventHandler(priority = HIGHEST, ignoreCancelled = true) - fun anvilCombineCheck(event: PrepareAnvilEvent) { - // Test if the event should bypass custom anvil. - if(DependencyManager.tryEventPreAnvilBypass(event)) return - - val inventory = event.inventory - val first = inventory.getItem(ANVIL_INPUT_LEFT) ?: return - val second = inventory.getItem(ANVIL_INPUT_RIGHT) - - // Should find player - val player = event.view.player - if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return - - // Test custom recipe - val recipe = getCustomRecipe(first, second) - CustomAnvil.verboseLog("custom recipe not null? ${recipe != null}") - if(recipe != null){ - val amount = getCustomRecipeAmount(recipe, first, second) - - val resultItem: ItemStack = recipe.resultItem!!.clone() - resultItem.amount *= amount - - event.result = resultItem - setAnvilInvXp(inventory, event.view, recipe.xpCostPerCraft * amount, true) - - return - } - - // Test rename lonely item - if (second == null) { - val resultItem = first.clone() - var anvilCost = handleRename(resultItem, inventory, player) - - // Test/stop if nothing changed. - if (first == resultItem) { - CustomAnvil.log("no right item, But input is same as output") - event.result = null - return - } - - event.result = resultItem - - anvilCost += calculatePenalty(first, null, resultItem) - - setAnvilInvXp(inventory, event.view, anvilCost) - return - } - - // Test for merge - if (first.canMergeWith(second)) { - val newEnchants = first.findEnchantments() - .combineWith(second.findEnchantments(), first, player) - val resultItem = first.clone() - resultItem.setEnchantmentsUnsafe(newEnchants) - - // Calculate enchantment cost - var anvilCost = 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 - } - - // Test/stop if nothing changed. - if (first == resultItem) { - CustomAnvil.log("Mergable with second, But input is same as output") - event.result = null - return - } - // As calculatePenalty edit result, we need to calculate penalty after checking equality - anvilCost += calculatePenalty(first, second, resultItem) - // Calculate rename cost - anvilCost += handleRename(resultItem, inventory, player) - - // Finally, we set result - event.result = resultItem - - setAnvilInvXp(inventory, event.view, anvilCost) - return - } - - // Test for unit repair - val unitRepairAmount = first.getRepair(second) - if (unitRepairAmount != null) { - val resultItem = first.clone() - var anvilCost = handleRename(resultItem, inventory, player) - - val repairAmount = resultItem.unitRepair(second.amount, unitRepairAmount) - if (repairAmount > 0) { - anvilCost += repairAmount * ConfigOptions.unitRepairCost - } - // We do not care about right item penalty for unit repair - anvilCost += calculatePenalty(first, null, resultItem, true) - - // Test/stop if nothing changed. - if (first == resultItem) { - CustomAnvil.log("unit repair, But input is same as output") - event.result = null - return - } - event.result = resultItem - - setAnvilInvXp(inventory, event.view, anvilCost) - } else { - CustomAnvil.log("no anvil fuse type found") - event.result = null - } - - } - - private fun handleRename(resultItem: ItemStack, inventory: AnvilInventory, player: HumanEntity): Int { - // Rename item and add renaming cost - resultItem.itemMeta?.let { - val displayName = ChatColor.stripColor(it.displayName) - var inventoryName = ChatColor.stripColor(inventory.renameText) - - var sumCost = 0 - - var useColor = false - if(ConfigOptions.renameColorPossible){ - val resultString = StringBuilder(inventoryName) - - useColor = handleRenamingColor(resultString, player) - - if(useColor) { - inventoryName = resultString.toString() - - sumCost+= ConfigOptions.useOfColorCost - } - } - - if ((!useColor && (!displayName.contentEquals(inventoryName))) || (useColor && !(it.displayName).contentEquals(inventoryName))) { - it.setDisplayName(inventoryName) - resultItem.itemMeta = it - - sumCost+= ConfigOptions.itemRenameCost - } - - return sumCost - } - return 0 - } - - private fun handleRenamingColor(textToColor: StringBuilder, player: HumanEntity): Boolean { - val usePermission = ConfigOptions.permissionNeededForColor - val canUseColorCode = ConfigOptions.allowColorCode && (!usePermission || player.hasPermission("ca.color.code")) - val canUseHexColor = ConfigOptions.allowHexadecimalColor && (!usePermission || player.hasPermission("ca.color.hex")) - - if((!canUseColorCode) && (!canUseHexColor)) return false - - var useColor = false - // Handle color code - if(canUseColorCode){ - var nbReplacement = replaceAll(textToColor, "&", "§", 2) - nbReplacement -= 2 * replaceAll(textToColor, "§§", "&", 2) - - if(nbReplacement > 0) useColor = true - } - - if(canUseHexColor){ - val nbReplacement = replaceHexToColor(textToColor, 7) - - if(nbReplacement > 0) useColor = true - } - - return useColor - } - - /** - * Replace every instance of "from" to "to". - * @param builder The builder to replace the string from. - * @param from The source that should be replaced. - * @param to The string that should replace. - * @param endOffset Amount of character that should be ignored at the end. - * @return The number of replacement was that was done. - */ - private fun replaceAll(builder: java.lang.StringBuilder, from: String, to: String, endOffset: Int): Int { - var index = builder.indexOf(from) - var numberOfChanges = 0 - - while (index != -1 && index < builder.length - endOffset) { - builder.replace(index, index + from.length, to) - index += to.length - index = builder.indexOf(from, index) - - numberOfChanges+=1 - } - - return numberOfChanges - } - - val HEX_PATTERN: Pattern = Pattern.compile("#[A-Fa-f0-9]{6}") // pattern to find hexadecimal string - /** - * Replace every hex color formatted like #000000 to the minecraft format - * @param builder The builder to replace the hex color from. - * @param endOffset Amount of character that should be ignored at the end. - * @return The number of replacement was that was done. - */ - private fun replaceHexToColor(builder: StringBuilder, endOffset: Int): Int { - val matcher: Matcher = HEX_PATTERN.matcher(builder) - - var numberOfChanges = 0 - var startIndex = 0 - - while(matcher.find(startIndex)){ - startIndex = matcher.start() - if(startIndex >= builder.length - endOffset) break - - builder.replace(startIndex, startIndex + 1, "§x") - startIndex+=2 - for (i in 0..5) { - builder.insert(startIndex, '§') - startIndex+=2 - } - - numberOfChanges+=1 - } - - return numberOfChanges - } - - /** - * Event handler logic for when a player is trying to pull an item out of the anvil - */ - @EventHandler(ignoreCancelled = true) - fun anvilExtractionCheck(event: InventoryClickEvent) { - val player = event.whoClicked as? Player ?: return - if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return - val inventory = event.inventory as? AnvilInventory ?: return - - if (event.rawSlot != ANVIL_OUTPUT_SLOT) { - return - } - // Test if the event should bypass custom anvil. - if(DependencyManager.tryClickAnvilResultBypass(event, inventory)) return - - val output = inventory.getItem(ANVIL_OUTPUT_SLOT) ?: return - val leftItem = inventory.getItem(ANVIL_INPUT_LEFT) ?: return - val rightItem = inventory.getItem(ANVIL_INPUT_RIGHT) - - // Test custom recipe - val recipe = getCustomRecipe(leftItem, rightItem) - if(recipe != null){ - event.result = Event.Result.ALLOW - onCustomCraft( - event, recipe, player, - leftItem, rightItem, output, inventory) - return - } - - val canMerge = leftItem.canMergeWith(rightItem) - val unitRepairResult = leftItem.getRepair(rightItem) - val allowed = (rightItem == null) - || (canMerge) - || (unitRepairResult != null) - - // True if there was no change or not allowed - if ((output == inventory.getItem(ANVIL_INPUT_LEFT)) - || !allowed - ) { - event.result = Event.Result.DENY - return - } - if (rightItem == null) { - event.result = Event.Result.ALLOW - return - } - if (canMerge) { - event.result = Event.Result.ALLOW - } else if (unitRepairResult != null) { - onUnitRepairExtract( - leftItem, rightItem, output, - unitRepairResult, event, player, inventory - ) - - return - } - } - - private fun onCustomCraft(event: InventoryClickEvent, - recipe: AnvilCustomRecipe, - player: Player, - leftItem: ItemStack, - rightItem: ItemStack?, - output: ItemStack, - inventory: AnvilInventory) { - event.result = Event.Result.DENY - - if(recipe.leftItem == null) return // in case it changed - - val amount = getCustomRecipeAmount(recipe, leftItem, rightItem) - val xpCost = amount * recipe.xpCostPerCraft - - if ((player.gameMode != GameMode.CREATIVE) && (player.level < xpCost)) return - - // We give the item manually - // But first we check if we should give the item - val slotDestination = getActionSlot(event, player) - if (slotDestination.type == SlotType.NO_SLOT) return - - // If not creative middle click... - if (event.click != ClickType.MIDDLE) { - // We remove what should be removed - leftItem.amount -= amount * recipe.leftItem!!.amount - inventory.setItem(ANVIL_INPUT_LEFT, leftItem) - - if(rightItem != null){ - if(recipe.rightItem == null) return // in case it changed - - rightItem.amount -= amount * recipe.rightItem!!.amount - inventory.setItem(ANVIL_INPUT_RIGHT, rightItem) - } - - if(player.gameMode != GameMode.CREATIVE){ - player.level -= xpCost - } - - // Then we try to find the new values for the anvil - val newAmount = getCustomRecipeAmount(recipe, leftItem, rightItem) - - CustomAnvil.verboseLog("new amount is $newAmount") - if(newAmount <= 0 || recipe.exactCount){ - inventory.setItem(ANVIL_OUTPUT_SLOT, null) - }else{ - val resultItem: ItemStack = recipe.resultItem!!.clone() - resultItem.amount *= newAmount - - val newXp = newAmount * newAmount - - inventory.repairCost = newXp - event.view.setProperty(REPAIR_COST, newXp) - - inventory.setItem(ANVIL_OUTPUT_SLOT, resultItem) - - player.updateInventory() - } - } - - // Finally, we add the item to the player - if (slotDestination.type == SlotType.CURSOR) { - player.setItemOnCursor(output) - } else {// We assume SlotType == SlotType.INVENTORY - player.inventory.setItem(slotDestination.slot, output) - } - - - } - - 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 - ) - - // To avoid vanilla, we cancel the event for unit repair - event.result = Event.Result.DENY - event.isCancelled = true - // And we give the item manually - // But first we check if we should give the item - val slotDestination = getActionSlot(event, player) - if (slotDestination.type == SlotType.NO_SLOT) return - - // Test repair cost - var repairCost = 0 - if (player.gameMode != GameMode.CREATIVE) { - // Get repairCost - leftItem.itemMeta?.let { leftMeta -> - val leftName = leftMeta.displayName - output.itemMeta?.let { - // Rename cost - if (!leftName.contentEquals(it.displayName)) { - repairCost += ConfigOptions.itemRenameCost - - // Color cost - if(it.displayName.contains('§')){ - repairCost += ConfigOptions.useOfColorCost - } - } - } - } - - repairCost += calculatePenalty(leftItem, null, resultCopy) - repairCost += resultAmount * ConfigOptions.unitRepairCost - - if ( - !ConfigOptions.doRemoveCostLimit && - ConfigOptions.doCapCost) { - - repairCost = min(repairCost, ConfigOptions.maxAnvilCost) - } - - if ((inventory.maximumRepairCost <= repairCost) - || (player.level < repairCost) - ) return - } - // If not creative middle click... - if (event.click != ClickType.MIDDLE) { - // We remove what should be removed - inventory.setItem(ANVIL_INPUT_LEFT, null) - rightItem.amount -= resultAmount - inventory.setItem(ANVIL_INPUT_RIGHT, rightItem) - inventory.setItem(ANVIL_OUTPUT_SLOT, null) - player.level -= repairCost - } - - // Finally, we add the item to the player - if (slotDestination.type == SlotType.CURSOR) { - player.setItemOnCursor(output) - } else {// We assume SlotType == SlotType.INVENTORY - player.inventory.setItem(slotDestination.slot, output) - } - } - - /** - * Get the destination slot or "NO_SLOT" slot container if there is no slot available - */ - private fun getActionSlot(event: InventoryClickEvent, player: Player): SlotContainer { - if (event.isShiftClick) { - val inventory = player.inventory - val firstEmpty = inventory.firstEmpty() - if (firstEmpty == -1) { - return NO_SLOT - } - //check hotbare full - var slotIndex = 8 - while (slotIndex >= 0 && ((inventory.getItem(slotIndex)?.type ?: Material.AIR) != Material.AIR)) { - slotIndex-- - } - if (slotIndex >= 0) { - return SlotContainer(SlotType.INVENTORY, slotIndex) - } - slotIndex = 35 //4*9 - 1 (max of player inventory) - while (slotIndex >= 9 && ((inventory.getItem(slotIndex)?.type ?: Material.AIR) != Material.AIR)) { - slotIndex-- - } - if (slotIndex < 9) { - return NO_SLOT - } - return SlotContainer(SlotType.INVENTORY, slotIndex) - } else { - if (player.itemOnCursor.type != Material.AIR) { - return NO_SLOT - } - return CURSOR_SLOT - } - } - - private fun getCustomRecipe ( - leftItem: ItemStack, - rightItem: ItemStack?) : AnvilCustomRecipe? { - - val recipeList = ConfigHolder.CUSTOM_RECIPE_HOLDER.recipeManager.recipeByMat[leftItem.type] ?: return null - - CustomAnvil.verboseLog("Testing " + recipeList.size+" recipe...") - for (recipe in recipeList) { - if(recipe.testItem(leftItem, rightItem)){ - return recipe - } - } - - return null - } - - private fun getCustomRecipeAmount( - recipe: AnvilCustomRecipe, - leftItem: ItemStack, - rightItem: ItemStack? - ): Int{ - return if(recipe.exactCount) { - if(leftItem.amount != recipe.leftItem!!.amount){ - 0 - }else if(rightItem != null && rightItem.amount != recipe.rightItem!!.amount){ - 0 - }else{ - 1 - } - } - else { - // test amount - val resultItem = recipe.resultItem!! // we know exist as the recipe was returned to us - val maxResultAmount = resultItem.type.maxStackSize/resultItem.amount - val maxLeftAmount = leftItem.amount/recipe.leftItem!!.amount - val maxRightAmount = if(rightItem == null){ maxLeftAmount } else{ rightItem.amount/recipe.rightItem!!.amount } - - CustomAnvil.verboseLog("resultItem: $resultItem, maxResultAmount: $maxResultAmount, maxLeftAmount: $maxLeftAmount, maxRightAmount: $maxRightAmount") - - min(min(maxResultAmount, maxLeftAmount), maxRightAmount) - } - } - - - @EventHandler - fun onAnvilClose(event: InventoryCloseEvent){ - val player = event.player - if(event.inventory !is AnvilInventory) return - if(player is Player && GameMode.CREATIVE != player.gameMode){ - packetManager.setInstantBuild(player, false) - } - - } - -} - - - - -private class SlotContainer(val type: SlotType, val slot: Int) -private enum class SlotType { - CURSOR, - INVENTORY, - NO_SLOT - -} diff --git a/src/main/kotlin/io/delilaheve/CustomAnvil.kt b/src/main/kotlin/io/delilaheve/CustomAnvil.kt index cbeb5cd..285e1c5 100644 --- a/src/main/kotlin/io/delilaheve/CustomAnvil.kt +++ b/src/main/kotlin/io/delilaheve/CustomAnvil.kt @@ -13,7 +13,10 @@ import xyz.alexcrea.cuanvil.dependency.DependencyManager import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry import xyz.alexcrea.cuanvil.gui.config.MainConfigGui import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant +import xyz.alexcrea.cuanvil.listener.AnvilCloseListener +import xyz.alexcrea.cuanvil.listener.AnvilResultListener import xyz.alexcrea.cuanvil.listener.ChatEventListener +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener import xyz.alexcrea.cuanvil.update.PluginSetDefault import xyz.alexcrea.cuanvil.update.Update_1_21 import xyz.alexcrea.cuanvil.update.plugin.PluginUpdates @@ -110,7 +113,9 @@ class CustomAnvil : JavaPlugin() { DependencyManager.loadDependency() // Register anvil events - server.pluginManager.registerEvents(AnvilEventListener(DependencyManager.packetManager), this) + server.pluginManager.registerEvents(PrepareAnvilListener(), this) + server.pluginManager.registerEvents(AnvilResultListener(), this) + server.pluginManager.registerEvents(AnvilCloseListener(DependencyManager.packetManager), this) // Load metrics Metrics(this, bstatsPluginId) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DisenchantmentDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DisenchantmentDependency.kt index be27c05..92ee146 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DisenchantmentDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DisenchantmentDependency.kt @@ -4,13 +4,13 @@ import cz.kominekjan.disenchantment.events.DisenchantClickEvent import cz.kominekjan.disenchantment.events.DisenchantEvent import cz.kominekjan.disenchantment.events.ShatterClickEvent import cz.kominekjan.disenchantment.events.ShatterEvent -import io.delilaheve.AnvilEventListener import io.delilaheve.CustomAnvil import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.event.inventory.PrepareAnvilEvent import org.bukkit.inventory.AnvilInventory import org.bukkit.inventory.ItemStack import org.bukkit.plugin.RegisteredListener +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener import xyz.alexcrea.cuanvil.util.AnvilXpUtil class DisenchantmentDependency { @@ -96,7 +96,7 @@ class DisenchantmentDependency { } fun testAnvilResult(event: InventoryClickEvent, inventory: AnvilInventory): Boolean { - val previousResultSlot = inventory.getItem(AnvilEventListener.ANVIL_OUTPUT_SLOT)?.clone() + val previousResultSlot = inventory.getItem(PrepareAnvilListener.ANVIL_OUTPUT_SLOT)?.clone() // Test event if change the result itemClickEvent.onDisenchantmentClickEvent(event) @@ -115,7 +115,7 @@ class DisenchantmentDependency { } private fun testAnvilInventoryChange(inventory: AnvilInventory, previous: ItemStack?): Boolean { - val currentResult = inventory.getItem(AnvilEventListener.ANVIL_OUTPUT_SLOT) + val currentResult = inventory.getItem(PrepareAnvilListener.ANVIL_OUTPUT_SLOT) return currentResult == previous } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilCloseListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilCloseListener.kt new file mode 100644 index 0000000..60a0339 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilCloseListener.kt @@ -0,0 +1,23 @@ +package xyz.alexcrea.cuanvil.listener + +import org.bukkit.GameMode +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.inventory.InventoryCloseEvent +import org.bukkit.inventory.AnvilInventory +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager + +class AnvilCloseListener(private val packetManager: PacketManager) : Listener { + + @EventHandler + fun onAnvilClose(event: InventoryCloseEvent){ + val player = event.player + if(event.inventory !is AnvilInventory) return + if(player is Player && GameMode.CREATIVE != player.gameMode){ + packetManager.setInstantBuild(player, false) + } + + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt new file mode 100644 index 0000000..fbc603d --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -0,0 +1,292 @@ +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 +import org.bukkit.event.Event +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.inventory.ClickType +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.inventory.AnvilInventory +import org.bukkit.inventory.InventoryView +import org.bukkit.inventory.ItemStack +import xyz.alexcrea.cuanvil.dependency.DependencyManager +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.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.CustomRecipeUtil +import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair +import kotlin.math.min + +class AnvilResultListener: Listener { + + companion object { + // static slot container + private val NO_SLOT = SlotContainer(SlotType.NO_SLOT, 0) + private val CURSOR_SLOT = SlotContainer(SlotType.CURSOR, 0) + } + + /** + * Event handler logic for when a player is trying to pull an item out of the anvil + */ + @EventHandler(ignoreCancelled = true) + fun anvilExtractionCheck(event: InventoryClickEvent) { + val player = event.whoClicked as? Player ?: return + if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return + val inventory = event.inventory as? AnvilInventory ?: return + + if (event.rawSlot != ANVIL_OUTPUT_SLOT) { + return + } + // Test if the event should bypass custom anvil. + if(DependencyManager.tryClickAnvilResultBypass(event, inventory)) return + + val output = inventory.getItem(ANVIL_OUTPUT_SLOT) ?: return + val leftItem = inventory.getItem(ANVIL_INPUT_LEFT) ?: return + val rightItem = inventory.getItem(ANVIL_INPUT_RIGHT) + + // Test custom recipe + val recipe = CustomRecipeUtil.getCustomRecipe(leftItem, rightItem) + if(recipe != null){ + event.result = Event.Result.ALLOW + onCustomCraft( + event, recipe, player, + leftItem, rightItem, output, inventory) + return + } + + val canMerge = leftItem.canMergeWith(rightItem) + val unitRepairResult = leftItem.getRepair(rightItem) + val allowed = (rightItem == null) + || (canMerge) + || (unitRepairResult != null) + + // True if there was no change or not allowed + if ((output == inventory.getItem(ANVIL_INPUT_LEFT)) + || !allowed + ) { + event.result = Event.Result.DENY + return + } + if (rightItem == null) { + event.result = Event.Result.ALLOW + return + } + if (canMerge) { + event.result = Event.Result.ALLOW + } else if (unitRepairResult != null) { + onUnitRepairExtract( + leftItem, rightItem, output, + unitRepairResult, event, player, inventory + ) + + return + } + } + + + private fun onCustomCraft(event: InventoryClickEvent, + recipe: AnvilCustomRecipe, + player: Player, + leftItem: ItemStack, + rightItem: ItemStack?, + output: ItemStack, + inventory: AnvilInventory + ) { + event.result = Event.Result.DENY + + if(recipe.leftItem == null) return // in case it changed + + val amount = CustomRecipeUtil.getCustomRecipeAmount(recipe, leftItem, rightItem) + val xpCost = amount * recipe.xpCostPerCraft + + if ((player.gameMode != GameMode.CREATIVE) && (player.level < xpCost)) return + + // We give the item manually + // But first we check if we should give the item + val slotDestination = getActionSlot(event, player) + if (slotDestination.type == SlotType.NO_SLOT) return + + // Handle not creative middle click... + if (event.click != ClickType.MIDDLE && + !handleCustomCraftClick(event, recipe, inventory, player, leftItem, rightItem, amount, xpCost)) return; + + // Finally, we add the item to the player + if (slotDestination.type == SlotType.CURSOR) { + player.setItemOnCursor(output) + } else {// We assume SlotType == SlotType.INVENTORY + player.inventory.setItem(slotDestination.slot, output) + } + } + + private fun handleCustomCraftClick(event: InventoryClickEvent, recipe: AnvilCustomRecipe, + inventory: AnvilInventory, player: Player, + leftItem: ItemStack, rightItem: ItemStack?, + amount: Int, xpCost: Int): Boolean { + // We remove what should be removed + if(rightItem != null){ + if(recipe.rightItem == null) return false// in case it changed + + rightItem.amount -= amount * recipe.rightItem!!.amount + inventory.setItem(ANVIL_INPUT_RIGHT, rightItem) + } + + leftItem.amount -= amount * recipe.leftItem!!.amount + inventory.setItem(ANVIL_INPUT_LEFT, leftItem) + + if(player.gameMode != GameMode.CREATIVE){ + player.level -= xpCost + } + + // Then we try to find the new values for the anvil + val newAmount = CustomRecipeUtil.getCustomRecipeAmount(recipe, leftItem, rightItem) + + CustomAnvil.verboseLog("new amount is $newAmount") + if(newAmount <= 0 || recipe.exactCount){ + inventory.setItem(ANVIL_OUTPUT_SLOT, null) + }else{ + val resultItem: ItemStack = recipe.resultItem!!.clone() + resultItem.amount *= newAmount + + val newXp = newAmount * newAmount + + inventory.repairCost = newXp + event.view.setProperty(InventoryView.Property.REPAIR_COST, newXp) + + inventory.setItem(ANVIL_OUTPUT_SLOT, resultItem) + + player.updateInventory() + } + return true + } + + private fun 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 + ) + + // To avoid vanilla, we cancel the event for unit repair + event.result = Event.Result.DENY + event.isCancelled = true + // And we give the item manually + // But first we check if we should give the item + val slotDestination = getActionSlot(event, player) + if (slotDestination.type == SlotType.NO_SLOT) return + + // Test repair cost + val repairCost = getUnitRepairCost(inventory, player, leftItem, output, resultCopy, resultAmount) + if(repairCost == Int.MIN_VALUE) return + + // If not creative middle click... + if (event.click != ClickType.MIDDLE) { + // We remove what should be removed + inventory.setItem(ANVIL_INPUT_LEFT, null) + rightItem.amount -= resultAmount + inventory.setItem(ANVIL_INPUT_RIGHT, rightItem) + inventory.setItem(ANVIL_OUTPUT_SLOT, null) + player.level -= repairCost + } + + // Finally, we add the item to the player + if (slotDestination.type == SlotType.CURSOR) { + player.setItemOnCursor(output) + } else {// We assume SlotType == SlotType.INVENTORY + player.inventory.setItem(slotDestination.slot, output) + } + } + + private fun getUnitRepairCost(inventory: AnvilInventory, player: Player, + leftItem: ItemStack, output: ItemStack, + resultCopy: ItemStack, resultAmount: Int): Int { + if (player.gameMode == GameMode.CREATIVE) return 0 + + var repairCost = 0; + // Get repairCost + leftItem.itemMeta?.let { leftMeta -> + val leftName = leftMeta.displayName + output.itemMeta?.let { + // Rename cost + if (!leftName.contentEquals(it.displayName)) { + repairCost += ConfigOptions.itemRenameCost + + // Color cost + if(it.displayName.contains('§')){ + repairCost += ConfigOptions.useOfColorCost + } + } + } + } + + repairCost += AnvilXpUtil.calculatePenalty(leftItem, null, resultCopy) + repairCost += resultAmount * ConfigOptions.unitRepairCost + + if ( + !ConfigOptions.doRemoveCostLimit && + ConfigOptions.doCapCost + ) { + repairCost = min(repairCost, ConfigOptions.maxAnvilCost) + } + + if ((inventory.maximumRepairCost <= repairCost) + || (player.level < repairCost) + ) return Int.MIN_VALUE + + return repairCost + } + + /** + * Get the destination slot or "NO_SLOT" slot container if there is no slot available + */ + private fun getActionSlot(event: InventoryClickEvent, player: Player): SlotContainer { + if (event.isShiftClick) { + val inventory = player.inventory + val firstEmpty = inventory.firstEmpty() + if (firstEmpty == -1) { + return NO_SLOT + } + //check hotbare full + var slotIndex = 8 + while (slotIndex >= 0 && ((inventory.getItem(slotIndex)?.type ?: Material.AIR) != Material.AIR)) { + slotIndex-- + } + if (slotIndex >= 0) { + return SlotContainer(SlotType.INVENTORY, slotIndex) + } + slotIndex = 35 //4*9 - 1 (max of player inventory) + while (slotIndex >= 9 && ((inventory.getItem(slotIndex)?.type ?: Material.AIR) != Material.AIR)) { + slotIndex-- + } + if (slotIndex < 9) { + return NO_SLOT + } + return SlotContainer(SlotType.INVENTORY, slotIndex) + } + else if (player.itemOnCursor.type != Material.AIR) return NO_SLOT + return CURSOR_SLOT + } + + private class SlotContainer(val type: SlotType, val slot: Int) + private enum class SlotType { + CURSOR, + INVENTORY, + NO_SLOT + + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt new file mode 100644 index 0000000..57de82f --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -0,0 +1,206 @@ +package xyz.alexcrea.cuanvil.listener + +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.entity.HumanEntity +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.inventory.PrepareAnvilEvent +import org.bukkit.inventory.AnvilInventory +import org.bukkit.inventory.ItemStack +import xyz.alexcrea.cuanvil.dependency.DependencyManager +import xyz.alexcrea.cuanvil.util.AnvilColorUtil +import xyz.alexcrea.cuanvil.util.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.CustomRecipeUtil +import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair + +/** + * Listener for anvil events + */ +class PrepareAnvilListener : Listener { + + companion object { + + // Anvil's output slot + const val ANVIL_INPUT_LEFT = 0 + const val ANVIL_INPUT_RIGHT = 1 + const val ANVIL_OUTPUT_SLOT = 2 + } + + /** + * Event handler logic for when an anvil contains items to be combined + */ + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + fun anvilCombineCheck(event: PrepareAnvilEvent) { + // Test if the event should bypass custom anvil. + if(DependencyManager.tryEventPreAnvilBypass(event)) return + + val inventory = event.inventory + val first = inventory.getItem(ANVIL_INPUT_LEFT) ?: return + val second = inventory.getItem(ANVIL_INPUT_RIGHT) + + // Should find player + val player = event.view.player + if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return + + // Test custom recipe + if(testCustomRecipe(event, inventory, first, second)) return + + // Test rename lonely item + if(second == null) { + doRenaming(event, inventory, player, first) + return + } + + // Test for merge + if (first.canMergeWith(second)) { + doMerge(event, inventory, player, first, second) + return + } + + // Test for unit repair + if(testUnitRepair(event, inventory, player, first, second)) return + + CustomAnvil.log("no anvil fuse type found") + event.result = null + + } + + private fun testCustomRecipe(event: PrepareAnvilEvent, inventory: AnvilInventory, first: ItemStack, second: ItemStack?): Boolean { + val recipe = CustomRecipeUtil.getCustomRecipe(first, second) + CustomAnvil.verboseLog("custom recipe not null? ${recipe != null}") + if(recipe == null) return false + + val amount = CustomRecipeUtil.getCustomRecipeAmount(recipe, first, second) + + val resultItem: ItemStack = recipe.resultItem!!.clone() + resultItem.amount *= amount + + event.result = resultItem + AnvilXpUtil.setAnvilInvXp(inventory, event.view, recipe.xpCostPerCraft * amount, true) + + return true + } + + private fun doRenaming(event: PrepareAnvilEvent, inventory: AnvilInventory, + player: HumanEntity, first: ItemStack) { + val resultItem = first.clone() + var anvilCost = handleRename(resultItem, inventory, player) + + // Test/stop if nothing changed. + if (first == resultItem) { + CustomAnvil.log("no right item, But input is same as output") + event.result = null + return + } + + event.result = resultItem + + anvilCost += AnvilXpUtil.calculatePenalty(first, null, resultItem) + + AnvilXpUtil.setAnvilInvXp(inventory, event.view, anvilCost) + } + + private fun handleRename(resultItem: ItemStack, inventory: AnvilInventory, player: HumanEntity): Int { + // Rename item and add renaming cost + resultItem.itemMeta?.let { + val displayName = ChatColor.stripColor(it.displayName) + var inventoryName = ChatColor.stripColor(inventory.renameText) + + var sumCost = 0 + + var useColor = false + if(ConfigOptions.renameColorPossible){ + val resultString = StringBuilder(inventoryName) + + useColor = AnvilColorUtil.handleRenamingColor(resultString, player) + + if(useColor) { + inventoryName = resultString.toString() + + sumCost+= ConfigOptions.useOfColorCost + } + } + + if ((!useColor && (!displayName.contentEquals(inventoryName))) || (useColor && !(it.displayName).contentEquals(inventoryName))) { + it.setDisplayName(inventoryName) + resultItem.itemMeta = it + + sumCost+= ConfigOptions.itemRenameCost + } + + return sumCost + } + return 0 + } + + private fun doMerge(event: PrepareAnvilEvent, inventory: AnvilInventory, + player: HumanEntity, + first: ItemStack, second: ItemStack) { + val newEnchants = first.findEnchantments() + .combineWith(second.findEnchantments(), first, player) + val resultItem = first.clone() + resultItem.setEnchantmentsUnsafe(newEnchants) + + // Calculate enchantment cost + var anvilCost = 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 + } + + // Test/stop if nothing changed. + if (first == resultItem) { + CustomAnvil.log("Mergable with second, But input is same as output") + event.result = null + return + } + // As calculatePenalty edit result, we need to calculate penalty after checking equality + anvilCost += AnvilXpUtil.calculatePenalty(first, second, resultItem) + // Calculate rename cost + anvilCost += handleRename(resultItem, inventory, player) + + // Finally, we set result + event.result = resultItem + + AnvilXpUtil.setAnvilInvXp(inventory, event.view, anvilCost) + } + + private fun testUnitRepair(event: PrepareAnvilEvent, inventory: AnvilInventory, player: HumanEntity, + first: ItemStack, second: ItemStack): Boolean { + val unitRepairAmount = first.getRepair(second) ?: return false + + val resultItem = first.clone() + var anvilCost = handleRename(resultItem, inventory, player) + + val repairAmount = resultItem.unitRepair(second.amount, unitRepairAmount) + if (repairAmount > 0) { + anvilCost += repairAmount * ConfigOptions.unitRepairCost + } + // We do not care about right item penalty for unit repair + anvilCost += AnvilXpUtil.calculatePenalty(first, null, resultItem, true) + + // 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 = resultItem + + AnvilXpUtil.setAnvilInvXp(inventory, event.view, anvilCost) + return true + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilColorUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilColorUtil.kt new file mode 100644 index 0000000..0e216e8 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilColorUtil.kt @@ -0,0 +1,88 @@ +package xyz.alexcrea.cuanvil.util + +import io.delilaheve.util.ConfigOptions +import org.bukkit.entity.HumanEntity +import java.util.regex.Matcher +import java.util.regex.Pattern + +object AnvilColorUtil { + private val HEX_PATTERN: Pattern = Pattern.compile("#[A-Fa-f0-9]{6}") // pattern to find hexadecimal string + + fun handleRenamingColor(textToColor: StringBuilder, player: HumanEntity): Boolean { + val usePermission = ConfigOptions.permissionNeededForColor + val canUseColorCode = ConfigOptions.allowColorCode && (!usePermission || player.hasPermission("ca.color.code")) + val canUseHexColor = ConfigOptions.allowHexadecimalColor && (!usePermission || player.hasPermission("ca.color.hex")) + + if((!canUseColorCode) && (!canUseHexColor)) return false + + var useColor = false + // Handle color code + if(canUseColorCode){ + var nbReplacement = replaceAll(textToColor, "&", "§", 2) + nbReplacement -= 2 * replaceAll(textToColor, "§§", "&", 2) + + if(nbReplacement > 0) useColor = true + } + + if(canUseHexColor){ + val nbReplacement = replaceHexToColor(textToColor, 7) + + if(nbReplacement > 0) useColor = true + } + + return useColor + } + + /** + * Replace every instance of "from" to "to". + * @param builder The builder to replace the string from. + * @param from The source that should be replaced. + * @param to The string that should replace. + * @param endOffset Amount of character that should be ignored at the end. + * @return The number of replacement was that was done. + */ + private fun replaceAll(builder: java.lang.StringBuilder, from: String, to: String, endOffset: Int): Int { + var index = builder.indexOf(from) + var numberOfChanges = 0 + + while (index != -1 && index < builder.length - endOffset) { + builder.replace(index, index + from.length, to) + index += to.length + index = builder.indexOf(from, index) + + numberOfChanges+=1 + } + + return numberOfChanges + } + + /** + * Replace every hex color formatted like #000000 to the minecraft format + * @param builder The builder to replace the hex color from. + * @param endOffset Amount of character that should be ignored at the end. + * @return The number of replacement was that was done. + */ + private fun replaceHexToColor(builder: StringBuilder, endOffset: Int): Int { + val matcher: Matcher = HEX_PATTERN.matcher(builder) + + var numberOfChanges = 0 + var startIndex = 0 + + while(matcher.find(startIndex)){ + startIndex = matcher.start() + if(startIndex >= builder.length - endOffset) break + + builder.replace(startIndex, startIndex + 1, "§x") + startIndex+=2 + for (i in 0..5) { + builder.insert(startIndex, '§') + startIndex+=2 + } + + numberOfChanges+=1 + } + + return numberOfChanges + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/CustomRecipeUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/CustomRecipeUtil.kt new file mode 100644 index 0000000..3ec5e71 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/CustomRecipeUtil.kt @@ -0,0 +1,54 @@ +package xyz.alexcrea.cuanvil.util + +import io.delilaheve.CustomAnvil +import org.bukkit.inventory.ItemStack +import xyz.alexcrea.cuanvil.config.ConfigHolder +import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe +import kotlin.math.min + +object CustomRecipeUtil { + + fun getCustomRecipe ( + leftItem: ItemStack, + rightItem: ItemStack?) : AnvilCustomRecipe? { + + val recipeList = ConfigHolder.CUSTOM_RECIPE_HOLDER.recipeManager.recipeByMat[leftItem.type] ?: return null + + CustomAnvil.verboseLog("Testing " + recipeList.size + " recipe...") + for (recipe in recipeList) { + if(recipe.testItem(leftItem, rightItem)){ + return recipe + } + } + + return null + } + + fun getCustomRecipeAmount( + recipe: AnvilCustomRecipe, + leftItem: ItemStack, + rightItem: ItemStack? + ): Int{ + return if(recipe.exactCount) { + if(leftItem.amount != recipe.leftItem!!.amount){ + 0 + }else if(rightItem != null && rightItem.amount != recipe.rightItem!!.amount){ + 0 + }else{ + 1 + } + } + else { + // test amount + val resultItem = recipe.resultItem!! // we know exist as the recipe was returned to us + val maxResultAmount = resultItem.type.maxStackSize/resultItem.amount + val maxLeftAmount = leftItem.amount/recipe.leftItem!!.amount + val maxRightAmount = if(rightItem == null){ maxLeftAmount } else{ rightItem.amount/recipe.rightItem!!.amount } + + CustomAnvil.verboseLog("resultItem: $resultItem, maxResultAmount: $maxResultAmount, maxLeftAmount: $maxLeftAmount, maxRightAmount: $maxRightAmount") + + min(min(maxResultAmount, maxLeftAmount), maxRightAmount) + } + } + +} \ No newline at end of file