diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/Rebar.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/Rebar.kt index d80e06a62..566373565 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/Rebar.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/Rebar.kt @@ -46,6 +46,7 @@ import io.github.pylonmc.rebar.recipe.RecipeType import io.github.pylonmc.rebar.registry.RebarRegistry import io.github.pylonmc.rebar.resourcepack.armor.ArmorTextureEngine import io.github.pylonmc.rebar.util.delayTicks +import io.github.pylonmc.rebar.item.base.VanillaAnvilItem import io.github.pylonmc.rebar.util.mergeGlobalConfig import io.github.pylonmc.rebar.waila.Waila import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder @@ -222,6 +223,7 @@ object Rebar : JavaPlugin(), RebarAddon { RebarSplashPotion.register(this, pm) RebarTool.register(this, pm) RebarWeapon.register(this, pm) + VanillaAnvilItem.register(this, pm) VanillaCookingFuel.register(this, pm) // Rebar Entities diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItem.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItem.kt index 3aa83f695..af4c7cf79 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItem.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItem.kt @@ -150,7 +150,7 @@ open class RebarItem(val stack: ItemStack) : Keyed { @Suppress("UNCHECKED_CAST") fun fromStack(stack: ItemStack?, clazz: Class): T? { val schema = RebarItemSchema.fromStack(stack) ?: return null - if (!clazz.isAssignableFrom(schema.itemClass)) return null + if (!schema.isType(clazz)) return null return schema.itemClass.cast(schema.loadConstructor.invoke(stack)) as T? } @@ -176,7 +176,7 @@ open class RebarItem(val stack: ItemStack) : Keyed { @Contract("null, _ -> false") fun isRebarItem(stack: ItemStack?, clazz: Class<*>): Boolean { val schema = RebarItemSchema.fromStack(stack) ?: return false - return clazz.isAssignableFrom(schema.itemClass) + return schema.isType(clazz) } /** diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItemSchema.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItemSchema.kt index ec81547f0..ae1345e62 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItemSchema.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItemSchema.kt @@ -53,6 +53,10 @@ class RebarItemSchema @JvmOverloads internal constructor( */ fun getRebarItem(): RebarItem = itemClass.cast(loadConstructor.invoke(getItemStack())) + fun isType(clazz: Class): Boolean { + return clazz.isAssignableFrom(itemClass) + } + val research: Research? get() = RebarRegistry.RESEARCHES.find { key in it.unlocks } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/base/RebarRepairable.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/base/RebarRepairable.kt new file mode 100644 index 000000000..f96ed2ed2 --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/base/RebarRepairable.kt @@ -0,0 +1,14 @@ +package io.github.pylonmc.rebar.item.base + +import io.github.pylonmc.rebar.item.RebarItemSchema +import org.bukkit.NamespacedKey +import org.bukkit.inventory.ItemStack + +interface RebarRepairable { + fun getRepairItems(): List + + fun isValidRepairItem(item: ItemStack): Boolean { + val key = RebarItemSchema.fromStack(item)?.key ?: item.type.key + return key in getRepairItems() + } +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/base/VanillaAnvilItem.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/base/VanillaAnvilItem.kt new file mode 100644 index 000000000..57ef532d3 --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/base/VanillaAnvilItem.kt @@ -0,0 +1,39 @@ +package io.github.pylonmc.rebar.item.base + +import io.github.pylonmc.rebar.event.api.MultiListener +import io.github.pylonmc.rebar.event.api.annotation.MultiHandlers +import io.github.pylonmc.rebar.event.api.annotation.UniversalHandler +import io.github.pylonmc.rebar.item.RebarItem +import io.github.pylonmc.rebar.item.RebarItemListener +import org.bukkit.event.EventPriority +import org.bukkit.event.inventory.PrepareAnvilEvent + +/** + * Enables the use of a RebarItem as if it's its vanilla counterpart in an anvil + * For example: If you have a rebar item that's an enchantment book, and you want it to be usable in an anvil + */ +interface VanillaAnvilItem { + fun onPrepareAnvilCraft(event: PrepareAnvilEvent) {} + + companion object : MultiListener { + @UniversalHandler + private fun onPrepareAnvil(event: PrepareAnvilEvent, priority: EventPriority) { + val rebarItem1 = RebarItem.fromStack(event.inventory.firstItem) + val rebarItem2 = RebarItem.fromStack(event.inventory.secondItem) + if (rebarItem1 is VanillaAnvilItem) { + try { + MultiHandlers.handleEvent(rebarItem1, "onPrepareAnvilCraft", event, priority) + } catch (e: Exception) { + RebarItemListener.logEventHandleErr(event, e, rebarItem1) + } + } + if (rebarItem2 is VanillaAnvilItem) { + try { + MultiHandlers.handleEvent(rebarItem2, "onPrepareAnvilCraft", event, priority) + } catch (e: Exception) { + RebarItemListener.logEventHandleErr(event, e, rebarItem2) + } + } + } + } +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeListener.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeListener.kt index 2682faf50..7d38def14 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeListener.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeListener.kt @@ -8,8 +8,11 @@ import io.github.pylonmc.rebar.recipe.vanilla.CookingRecipeWrapper import io.github.pylonmc.rebar.recipe.vanilla.ShapedRecipeType import io.github.pylonmc.rebar.recipe.vanilla.VanillaRecipeType import io.github.pylonmc.rebar.util.isRebarAndIsNot +import io.github.pylonmc.rebar.util.plainText import io.papermc.paper.datacomponent.DataComponentTypes import io.papermc.paper.event.player.CartographyItemEvent +import net.kyori.adventure.text.Component +import org.bukkit.GameMode import org.bukkit.Keyed import org.bukkit.block.Crafter import org.bukkit.block.Furnace @@ -25,6 +28,7 @@ import org.bukkit.inventory.ShapedRecipe import org.bukkit.inventory.ShapelessRecipe import org.bukkit.inventory.StonecutterInventory import kotlin.math.max +import kotlin.math.min internal object RebarRecipeListener : Listener { @@ -49,7 +53,7 @@ internal object RebarRecipeListener : Listener { var firstItem: ItemStack? = null var secondItem: ItemStack? = null for (item in e.inventory.matrix) { - if (item != null && !item.isEmpty) { + if (item != null && !item.isEmpty) { if (firstItem == null) { firstItem = item } else if (secondItem == null) { @@ -66,8 +70,9 @@ internal object RebarRecipeListener : Listener { val firstSchema = RebarItemSchema.fromStack(firstItem) val secondSchema = RebarItemSchema.fromStack(secondItem) if (firstSchema == null || secondSchema == null || firstSchema != secondSchema - || RebarUnmergeable::class.java.isAssignableFrom(firstSchema.itemClass) + || firstSchema.isType(RebarUnmergeable::class.java) || firstItem.amount != 1 || secondItem.amount != 1 + || firstItem.hasData(DataComponentTypes.UNBREAKABLE) || secondItem.hasData(DataComponentTypes.UNBREAKABLE) || !firstItem.hasData(DataComponentTypes.MAX_DAMAGE) || !secondItem.hasData(DataComponentTypes.MAX_DAMAGE) || !firstItem.hasData(DataComponentTypes.DAMAGE) || !secondItem.hasData(DataComponentTypes.DAMAGE)) { inventory.result = null @@ -245,21 +250,136 @@ internal object RebarRecipeListener : Listener { } } + @Suppress("UnstableApiUsage") @EventHandler(priority = EventPriority.LOWEST) private fun onAnvilSlotChanged(e: PrepareAnvilEvent) { + val player = e.viewers.first() as? Player ?: return val inventory = e.inventory val firstItem = inventory.firstItem val secondItem = inventory.secondItem - val firstRebarSchema = RebarItemSchema.fromStack(firstItem) - val secondRebarSchema = RebarItemSchema.fromStack(secondItem) - - // Disallow using Rebar items but allow renaming them - // Have tried to support this. It's really hard because you end up effectively - // having to do the repair manually for items that can't usually be repaired. - // Gave up after 2 hours or so - if ((firstRebarSchema != null && (secondItem != null && !secondItem.isEmpty)) || secondRebarSchema != null) { - e.result = null + val firstSchema = RebarItemSchema.fromStack(firstItem) + val secondSchema = RebarItemSchema.fromStack(secondItem) + + if (firstSchema == null && secondSchema == null) { + // If it's not a rebar item interaction, we don't care about it + return + } else if (firstSchema == null) { + // If it's a vanilla item being manipulated by a rebar item, prevent it unless it's a VanillaAnvilItem + if (secondSchema != null && !secondSchema.isType(VanillaAnvilItem::class.java)) { + e.result = null + } + return + } else if (secondItem == null || secondItem.isEmpty) { + // If it's renaming a rebar item, allow it, otherwise cancel + val resultItem = inventory.result + if (resultItem != null && !firstItem!!.matchesWithoutData(resultItem, setOf(DataComponentTypes.CUSTOM_NAME))) { + e.result = null + } + return + } else if (firstItem == null) { + // Dummy check return } + + var resultItem: ItemStack? = null + + // Allow repairing with rebar items + if (firstSchema.isType(RebarRepairable::class.java) && firstItem.isRepairable()) { + val repairable = RebarItem.fromStack(firstItem, RebarRepairable::class.java)!! + if (repairable.isValidRepairItem(secondItem)) { + var price = 0 + var namingPrice = 0 + val tax = firstItem.getDataOrDefault(DataComponentTypes.REPAIR_COST, 0)!! + secondItem.getDataOrDefault(DataComponentTypes.REPAIR_COST, 0)!! + var repairItemCountCost = 0 + + resultItem = firstItem.clone() + var damage = resultItem.getDataOrDefault(DataComponentTypes.DAMAGE, 0)!! + val maxDamage = resultItem.getDataOrDefault(DataComponentTypes.MAX_DAMAGE, 0)!! + var repairAmount = min(damage, maxDamage / 4) + if (repairAmount <= 0) { + e.result = null + return + } + + for (i in 0 until secondItem.amount) { + if (repairAmount <= 0) break + damage -= repairAmount + repairAmount = min(damage, maxDamage / 4) + repairItemCountCost++ + price++ + } + + val renameText = e.view.renameText + if (!renameText.isNullOrBlank()) { + if (renameText != resultItem.effectiveName().plainText) { + namingPrice = 1 + price += namingPrice + resultItem.setData(DataComponentTypes.CUSTOM_NAME, Component.text(renameText)) + } + } else if (resultItem.hasData(DataComponentTypes.CUSTOM_NAME)) { + namingPrice = 1 + price += namingPrice + resultItem.unsetData(DataComponentTypes.CUSTOM_NAME) + } + + if (price <= 0) { + e.result = null + return + } + + var cost = Math.clamp((tax + price).toLong(), 0, Int.MAX_VALUE) + if (namingPrice == price) { + if (cost >= 40) { + cost = 39 + } + } + + e.result = resultItem + e.view.repairCost = cost + e.view.repairItemCountCost = repairItemCountCost + + if (cost >= 40 && player.gameMode != GameMode.CREATIVE) { + e.result = null + return + } + + var resultCost = resultItem.getDataOrDefault(DataComponentTypes.REPAIR_COST, 0)!! + val secondCost = secondItem.getDataOrDefault(DataComponentTypes.REPAIR_COST, 0)!! + if (resultCost < secondCost) { + resultCost = secondCost + } + resultCost = min(resultCost * 2L + 1L, Int.MAX_VALUE.toLong()).toInt() + resultItem.setData(DataComponentTypes.DAMAGE, damage) + resultItem.setData(DataComponentTypes.REPAIR_COST, resultCost) + } + } + + // If it hasn't been repaired, check for enchantment application or item merging by piggy backing off of vanilla logic + if (resultItem == null) { + resultItem = e.result + if (resultItem == null) { + // Something else has already canceled it + return + } + + val usingBook = secondItem.hasData(DataComponentTypes.STORED_ENCHANTMENTS) + if (!usingBook && (firstSchema != secondSchema || firstSchema.isType(RebarUnmergeable::class.java))) { + e.result = null + } else if (!firstItem.matchesWithoutData(resultItem, setOf( + DataComponentTypes.ENCHANTMENTS, DataComponentTypes.STORED_ENCHANTMENTS, + DataComponentTypes.MAX_DAMAGE, DataComponentTypes.DAMAGE, + DataComponentTypes.CUSTOM_NAME, DataComponentTypes.REPAIR_COST + ))) { + e.result = null + } + } + } + + @Suppress("UnstableApiUsage") + private fun ItemStack.isRepairable(): Boolean { + return !hasData(DataComponentTypes.UNBREAKABLE) + && hasData(DataComponentTypes.MAX_DAMAGE) + && hasData(DataComponentTypes.DAMAGE) + && getData(DataComponentTypes.DAMAGE)!! > 0 } } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/RebarUtils.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/RebarUtils.kt index fda9cfff8..09ec13691 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/RebarUtils.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/RebarUtils.kt @@ -329,7 +329,7 @@ fun findRebarItemInInventory(inventory: Inventory, targetItem: RebarItem): Int? @JvmSynthetic inline fun ItemStack?.isRebarAndIsNot(): Boolean { val schema = RebarItemSchema.fromStack(this) - return schema != null && !T::class.java.isAssignableFrom(schema.itemClass) + return schema != null && !schema.isType(T::class.java) } @JvmSynthetic