diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/ItemTypeWrapper.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/ItemTypeWrapper.kt index afa042c9a..2146f36ec 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/ItemTypeWrapper.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/ItemTypeWrapper.kt @@ -1,6 +1,7 @@ package io.github.pylonmc.rebar.item import io.github.pylonmc.rebar.registry.RebarRegistry +import io.papermc.paper.datacomponent.DataComponentType import org.bukkit.* import org.bukkit.inventory.ItemStack @@ -10,6 +11,9 @@ import org.bukkit.inventory.ItemStack sealed interface ItemTypeWrapper : Keyed { fun createItemStack(): ItemStack + fun matchesWithoutData(item: ItemStack, excludeTypes: Set, ignoreCount: Boolean = false): Boolean + fun matchesWithoutData(item: ItemStack, excludeTypes: Set): Boolean = matchesWithoutData(item, excludeTypes, false) + fun isEmpty(): Boolean /** * The vanilla variant of [ItemTypeWrapper]. @@ -17,7 +21,10 @@ sealed interface ItemTypeWrapper : Keyed { @JvmRecord data class Vanilla(val material: Material) : ItemTypeWrapper { override fun createItemStack() = ItemStack(material) + override fun matchesWithoutData(item: ItemStack, excludeTypes: Set, ignoreCount: Boolean) = createItemStack().matchesWithoutData(item, excludeTypes, ignoreCount) override fun getKey() = material.key + override fun isEmpty() = material.isAir() + override fun toString() = "ItemTypeWrapper.Vanilla(${material.key})" } /** @@ -26,7 +33,10 @@ sealed interface ItemTypeWrapper : Keyed { @JvmRecord data class Rebar(val item: RebarItemSchema) : ItemTypeWrapper { override fun createItemStack() = item.getItemStack() + override fun matchesWithoutData(item: ItemStack, excludeTypes: Set, ignoreCount: Boolean) = this.item.matchesWithoutData(item, excludeTypes, ignoreCount) override fun getKey() = item.key + override fun isEmpty() = false + override fun toString() = "ItemTypeWrapper.Rebar(${item.key})" } companion object { 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 f22b63d93..b22451a4d 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 @@ -12,7 +12,7 @@ import io.github.pylonmc.rebar.util.findConstructorMatching import io.github.pylonmc.rebar.util.getAddon import io.github.pylonmc.rebar.util.position.position import io.github.pylonmc.rebar.util.rebarKey -import io.papermc.paper.command.brigadier.argument.ArgumentTypes.blockPosition +import io.papermc.paper.datacomponent.DataComponentType import org.bukkit.Keyed import org.bukkit.NamespacedKey import org.bukkit.inventory.ItemStack @@ -74,6 +74,10 @@ class RebarItemSchema @JvmOverloads internal constructor( return BlockStorage.setBlock(context.block.position, blockSchema, context) } + @JvmOverloads + fun matchesWithoutData(item: ItemStack, excludeTypes: Set, ignoreCount: Boolean = false) = + template.matchesWithoutData(item, excludeTypes, ignoreCount) + override fun getKey(): NamespacedKey = key override fun equals(other: Any?): Boolean = key == (other as? RebarItemSchema)?.key diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RebarRecipe.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RebarRecipe.kt index ca52855c0..3217211a4 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RebarRecipe.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RebarRecipe.kt @@ -2,9 +2,13 @@ package io.github.pylonmc.rebar.recipe import com.google.common.collect.MapMaker import io.github.pylonmc.rebar.fluid.RebarFluid +import io.github.pylonmc.rebar.util.LRUCache +import io.github.pylonmc.rebar.util.hashIgnoreAmount import org.bukkit.Keyed +import org.bukkit.NamespacedKey import org.bukkit.inventory.ItemStack import xyz.xenondevs.invui.gui.Gui +import java.util.function.Predicate interface RebarRecipe : Keyed { @@ -46,6 +50,7 @@ interface RebarRecipe : Keyed { companion object { private val priorities = MapMaker().weakKeys().makeMap() + private val cache = LRUCache(1000) @JvmStatic var RebarRecipe.priority: Double @@ -53,5 +58,110 @@ interface RebarRecipe : Keyed { set(value) { priorities[this] = value } + + private fun cacheKey(hash: Int, recipeType: RecipeType<*>) = 31 * hash + recipeType.hashCode() + + fun getCached(hash: Int, recipeType: RecipeType<*>) = cache[cacheKey(hash, recipeType)] + fun isCached(hash: Int, recipeType: RecipeType<*>) = cacheKey(hash, recipeType) in cache + fun cache(hash: Int, recipeType: RecipeType<*>, recipe: RebarRecipe?) { + cache[cacheKey(hash, recipeType)] = recipe + } + fun clearCache() { cache.clear() } + + inline fun searchRecipes(recipeType: RecipeType, hint: NamespacedKey?, hash: Int, pred: Predicate): T? { + // Try the hint (usu. what minecraft thinks we are trying to craft) + if (hint != null) { + val hintRecipe = recipeType.getRecipe(hint) + if (hintRecipe != null && pred.test(hintRecipe)) { + cache(hash, recipeType, hintRecipe) + return hintRecipe + } + } + // Try the cache + if (isCached(hash, recipeType)) { + // Null in the cache means no recipe was found + val cachedRecipe = getCached(hash, recipeType) ?: return null + if (cachedRecipe is T && pred.test(cachedRecipe)) return cachedRecipe + } + // Linear search + for (recipe in recipeType.recipes) { + if (pred.test(recipe)) { + cache(hash, recipeType, recipe) + return recipe + } + } + cache(hash, recipeType, null) + return null + } + + inline fun searchRecipes(recipeType: RecipeType, hash: Int, pred: Predicate): T? + = searchRecipes(recipeType, null, hash, pred) + + fun hashShaped2D(items: Iterable>): Int = + items.fold(1) { outerHash, row -> + row.fold(outerHash) { hash, item -> 31 * hash + (item?.hashIgnoreAmount() ?: 0) } + } + + fun hashShaped(items: Iterable): Int = + items.fold(1) { hash, i -> 31 * hash + (i?.hashIgnoreAmount() ?: 0) } + + fun hashShapeless2D(items: Iterable>): Int = + items.sumOf { row -> row.sumOf { it?.hashIgnoreAmount() ?: 0 } } + + fun hashShapeless(items: Iterable): Int = + items.sumOf { it?.hashIgnoreAmount() ?: 0 } + + fun matchesShaped(items: List, recipeInput: List, w: Int, h: Int): Boolean { + var i = -1 + var r = -1 + var iCurr: ItemStack? = null + var rCurr: RecipeInput.Item? = null + // Find first non-empty element of both lists + while (i+1 < items.size && iCurr?.isEmpty ?: true) { + i++ + iCurr = items[i] + } + while (r+1 < recipeInput.size && rCurr?.isEmpty() ?: true) { + r++ + rCurr = recipeInput[r] + } + // Get relative vertical and horizontal offsets + val vOffset = i/w - r/w + val hOffset = i%w - r%w + // The items match if either + // - both are empty + // - both match and are at the same relative vertical and horizontal offset as the first non-empty items + // we want all items to match until both lists are exhausted + while ( + (iCurr?.isEmpty ?: true && rCurr?.isEmpty() ?: true) || + (rCurr?.matches(iCurr) == true && i/w - r/w == vOffset && i%w - r%w == hOffset) + ) { + if (r >= recipeInput.size && i >= items.size) return true + iCurr = if (i < items.size) items[i] else null + rCurr = if (r < recipeInput.size) recipeInput[r] else null + i++ + r++ + } + return false + } + + fun matchesShapeless(items: List, recipeInput: List): Boolean { + if (items.count { !(it?.isEmpty ?: true) } != recipeInput.count { !it.isEmpty() }) return false + // Try to match each non-empty item to a recipe input + val matchedIndices = mutableSetOf() + outer@ for (item in items) { + if (item?.isEmpty ?: true) continue + for ((i, input) in recipeInput.withIndex()) { + if (!input.isEmpty() && i !in matchedIndices && input.matches(item)) { + matchedIndices.add(i) + continue@outer + } + } + // no match for item + return false + } + return true + } + } } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeInput.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeInput.kt index 3ea9128e0..17a700004 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeInput.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeInput.kt @@ -2,11 +2,14 @@ package io.github.pylonmc.rebar.recipe import io.github.pylonmc.rebar.fluid.RebarFluid import io.github.pylonmc.rebar.item.ItemTypeWrapper +import io.papermc.paper.datacomponent.DataComponentType +import org.bukkit.Material import org.bukkit.Tag import org.bukkit.inventory.ItemStack sealed interface RecipeInput { - data class Item(val items: MutableSet, val amount: Int) : RecipeInput { + data class Item(val items: MutableSet, val amount: Int, val ignoreComponents: MutableSet) : RecipeInput { + constructor(items: MutableSet, amount: Int) : this(items, amount, mutableSetOf()) constructor(amount: Int, vararg items: ItemStack) : this(items.mapTo(mutableSetOf()) { ItemTypeWrapper(it) }, amount) constructor(tag: Tag, amount: Int) : this(tag.values, amount) @@ -23,12 +26,24 @@ sealed interface RecipeInput { representativeItems.first() } - fun matches(itemStack: ItemStack): Boolean { - if (itemStack.amount < amount) return false + fun matches(itemStack: ItemStack?): Boolean { + if (itemStack?.isEmpty ?: true) return isEmpty() + if (isEmpty() || itemStack.amount < amount) return false return contains(itemStack) } - operator fun contains(itemStack: ItemStack): Boolean = ItemTypeWrapper(itemStack) in items + operator fun contains(itemStack: ItemStack): Boolean { + for (item in items) { + if (item.createItemStack().matchesWithoutData(itemStack, ignoreComponents, true)) { + return true + } + } + return false + } + + fun isEmpty(): Boolean { + return items.none{ !it.isEmpty() } + } } @JvmRecord @@ -50,6 +65,9 @@ sealed interface RecipeInput { } companion object { + @JvmStatic + val EMPTY_ITEM = Item(mutableSetOf(ItemTypeWrapper.Vanilla(Material.AIR)), 1) + @JvmStatic @JvmOverloads fun of(item: ItemStack, amount: Int = item.amount) = Item(amount, item) 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 e4ce51a30..2def5cf24 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 @@ -3,9 +3,9 @@ package io.github.pylonmc.rebar.recipe import io.github.pylonmc.rebar.item.RebarItem import io.github.pylonmc.rebar.item.base.* import io.github.pylonmc.rebar.item.research.Research.Companion.canCraft -import io.github.pylonmc.rebar.recipe.vanilla.CookingRecipeWrapper -import io.github.pylonmc.rebar.recipe.vanilla.ShapedRecipeType +import io.github.pylonmc.rebar.recipe.vanilla.CraftingRecipeWrapper import io.github.pylonmc.rebar.recipe.vanilla.VanillaRecipeType +import io.github.pylonmc.rebar.util.hashIgnoreAmount import io.github.pylonmc.rebar.util.isRebarAndIsNot import io.papermc.paper.datacomponent.DataComponentTypes import io.papermc.paper.event.player.CartographyItemEvent @@ -17,15 +17,15 @@ import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority import org.bukkit.event.Listener import org.bukkit.event.block.BlockCookEvent +import org.bukkit.event.block.CampfireStartEvent import org.bukkit.event.block.CrafterCraftEvent import org.bukkit.event.inventory.* import org.bukkit.inventory.ItemStack -import org.bukkit.inventory.ShapedRecipe -import org.bukkit.inventory.ShapelessRecipe import org.bukkit.inventory.StonecutterInventory internal object RebarRecipeListener : Listener { + @Suppress("UnstableApiUsage") @EventHandler(priority = EventPriority.LOWEST) private fun onPreCraft(e: PrepareItemCraftEvent) { val recipe = e.recipe @@ -34,11 +34,10 @@ internal object RebarRecipeListener : Listener { val inventory = e.inventory val hasRebarItems = inventory.any { it.isRebarAndIsNot() } - val isNotRebarCraftingRecipe = recipe.key in VanillaRecipeType.nonRebarRecipes - // Prevent the erroneous crafting of vanilla items with Rebar ingredients - if (hasRebarItems && isNotRebarCraftingRecipe) { - inventory.result = null + // If vanilla ingredients matched a vanilla recipe, we leave it + if (recipe.key in VanillaRecipeType.nonRebarRecipes && !hasRebarItems) { + return } // Allow merging Rebar tools/weapons/armour in crafting grid unless marked with RebarUnmergeable @@ -65,12 +64,31 @@ internal object RebarRecipeListener : Listener { val resultDamage = inventory.result!!.getData(DataComponentTypes.DAMAGE)!! result.setData(DataComponentTypes.DAMAGE, resultDamage) inventory.result = result + return } } else { inventory.result = null } } - + // Due to rebar ingredients possibly needing to ignore components (and thus using MaterialChoice) + // we can't fully trust that the recipe returned by MC is correct + var rebarRecipe: CraftingRecipeWrapper? = RebarRecipe.searchRecipes( + RecipeType.VANILLA_SHAPED, + recipe.key, + RebarRecipe.hashShaped(e.inventory.matrix.toList()) + ) { it.matches(e.inventory.matrix.toList()) } + if (rebarRecipe == null) { + // Try shapeless instead + rebarRecipe = RebarRecipe.searchRecipes( + RecipeType.VANILLA_SHAPELESS, + recipe.key, + RebarRecipe.hashShapeless(e.inventory.matrix.toList()) + ) { it.matches(e.inventory.matrix.toList()) } + } + if (rebarRecipe == null) { + inventory.result = null + return + } // Prevent crafting of unresearched items val rebarItemResult = RebarItem.fromStack(recipe.result) val anyViewerDoesNotHaveResearch = rebarItemResult != null && e.viewers.none { @@ -78,7 +96,9 @@ internal object RebarRecipeListener : Listener { } if (anyViewerDoesNotHaveResearch) { inventory.result = null + return } + inventory.result = rebarRecipe.craftingRecipe.result.clone() } @EventHandler(priority = EventPriority.LOWEST) @@ -95,65 +115,30 @@ internal object RebarRecipeListener : Listener { private fun onCrafterCraft(e: CrafterCraftEvent) { val crafterState = e.block.state as? Crafter ?: return val inventory = crafterState.inventory - - val hasRebarItems = inventory.any { it.isRebarAndIsNot() } - if (!hasRebarItems) { + var recipe: CraftingRecipeWrapper? = RebarRecipe.searchRecipes( + RecipeType.VANILLA_SHAPED, + e.recipe.key, + RebarRecipe.hashShaped(inventory.contents.toList()) + ) { it.matches(inventory.contents.toList()) } + if (recipe == null) { + // Try shapeless instead + recipe = RebarRecipe.searchRecipes( + RecipeType.VANILLA_SHAPELESS, + e.recipe.key, + RebarRecipe.hashShaped(inventory.contents.toList()) + ) { it.matches(inventory.contents.toList()) } + } + if (recipe == null) { + e.isCancelled = true return } - val crafter = e.block.state as Crafter - - // TODO make this not horrible (both for performance and readability) - see https://github.com/pylonmc/rebar/issues/545 - for (recipe in ShapedRecipeType.recipes) { - val craftingRecipe = recipe.craftingRecipe - if (craftingRecipe is ShapedRecipe) { - var i = 0 - var isValid = true - recipeLoop@ for (row in craftingRecipe.shape) { - for (index in row) { - val ingredient = craftingRecipe.choiceMap[index] - if (ingredient != null) { - val actual = crafter.inventory.getItem(i) - if (actual == null || !ingredient.test(actual)) { - isValid = false - break@recipeLoop - } - } - i++ - } - } - if (isValid) { - e.result = craftingRecipe.result - return - } - - } else if (craftingRecipe is ShapelessRecipe) { - val usedSlots = mutableSetOf() - for (ingredient in craftingRecipe.choiceList) { - var isValid = false - for (crafterIndex in 0..()) { - var rebarRecipe: CookingRecipeWrapper? = null - for (recipe in RecipeType.vanillaCookingRecipes()) { - if (recipe.key !in VanillaRecipeType.nonRebarRecipes && recipe.recipe.inputChoice.test(input)) { - rebarRecipe = recipe - break - } - } - val isFurnaceOutputValidToPutRecipeResultIn = rebarRecipe != null - && (furnace.inventory.result == null || rebarRecipe.isOutput(furnace.inventory.result!!)) - if (rebarRecipe == null || !isFurnaceOutputValidToPutRecipeResultIn) { - e.isCancelled = true - } + if (input == null) { + e.isCancelled = true + return + } + val recipeType = RecipeType.getCookingRecipeTypeByMaterial(e.block.type) + if (recipeType == null) { + e.isCancelled = true + return + } + val recipe = RebarRecipe.searchRecipes(recipeType, input.hashIgnoreAmount()) { it.matches(input) } + if (recipe == null) { + e.isCancelled = true + return + } + // The recipe is already valid because we searched on our end + val resultSlotItem = furnace.inventory.result + val canPlaceInOutput = resultSlotItem == null || (recipe.isOutput(resultSlotItem) && resultSlotItem.amount < resultSlotItem.maxStackSize) + if (!canPlaceInOutput) { + e.isCancelled = true } } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeType.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeType.kt index 7c051ff7b..8265862ff 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeType.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeType.kt @@ -4,6 +4,7 @@ import io.github.pylonmc.rebar.recipe.vanilla.* import io.github.pylonmc.rebar.registry.RebarRegistry import org.bukkit.Bukkit import org.bukkit.Keyed +import org.bukkit.Material import org.bukkit.NamespacedKey import org.bukkit.inventory.* import java.util.concurrent.ConcurrentHashMap @@ -28,10 +29,12 @@ open class RecipeType(private val key: NamespacedKey) : Keyed, open fun addRecipe(recipe: T) { registeredRecipes[recipe.key] = recipe + RebarRecipe.clearCache() } open fun removeRecipe(recipe: NamespacedKey) { registeredRecipes.remove(recipe) + RebarRecipe.clearCache() } fun register() { @@ -44,6 +47,16 @@ open class RecipeType(private val key: NamespacedKey) : Keyed, override fun getKey(): NamespacedKey = key + override fun hashCode(): Int { + return key.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return key == (other as RecipeType<*>).key + } + companion object { /** * Key: `minecraft:blasting` @@ -139,5 +152,14 @@ open class RecipeType(private val key: NamespacedKey) : Keyed, // @formatter:on } } + + @JvmStatic + fun getCookingRecipeTypeByMaterial(m: Material): VanillaRecipeType? = when (m) { + Material.FURNACE -> VANILLA_FURNACE + Material.BLAST_FURNACE -> VANILLA_BLASTING + Material.SMOKER -> VANILLA_SMOKING + Material.CAMPFIRE, Material.SOUL_CAMPFIRE -> VANILLA_CAMPFIRE + else -> null + } } } \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/vanilla/Cooking.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/vanilla/Cooking.kt index 635e8c8bc..32595c52e 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/vanilla/Cooking.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/vanilla/Cooking.kt @@ -4,11 +4,14 @@ import io.github.pylonmc.rebar.config.ConfigSection import io.github.pylonmc.rebar.config.adapter.ConfigAdapter import io.github.pylonmc.rebar.guide.button.ItemButton import io.github.pylonmc.rebar.i18n.RebarArgument +import io.github.pylonmc.rebar.item.base.VanillaCookingItem import io.github.pylonmc.rebar.item.builder.ItemStackBuilder import io.github.pylonmc.rebar.recipe.FluidOrItem import io.github.pylonmc.rebar.recipe.RecipeInput import io.github.pylonmc.rebar.util.gui.GuiItems import io.github.pylonmc.rebar.util.gui.unit.UnitFormat +import io.github.pylonmc.rebar.util.isRebarAndIsNot +import io.papermc.paper.datacomponent.DataComponentTypes import net.kyori.adventure.text.Component import org.bukkit.Material import org.bukkit.NamespacedKey @@ -16,11 +19,13 @@ import org.bukkit.inventory.* import org.bukkit.inventory.recipe.CookingBookCategory import xyz.xenondevs.invui.gui.Gui - -sealed class CookingRecipeWrapper(final override val recipe: CookingRecipe<*>) : VanillaRecipeWrapper { - override val inputs: List = listOf(recipe.inputChoice.asRecipeInput()) +sealed class CookingRecipeWrapper(final override val recipe: CookingRecipe<*>, val recipeInput: RecipeInput.Item) : VanillaRecipeWrapper { + override val inputs: List = listOf(recipeInput) override val results: List = listOf(FluidOrItem.of(recipe.result)) override fun getKey(): NamespacedKey = recipe.key + fun matches(item: ItemStack) = + if (item.isRebarAndIsNot() || key !in VanillaRecipeType.nonRebarRecipes) item in recipeInput + else recipe.inputChoice.test(item) protected abstract val displayBlock: Material @@ -51,36 +56,53 @@ sealed class CookingRecipeWrapper(final override val recipe: CookingRecipe<*>) : .build() } -class BlastingRecipeWrapper(recipe: BlastingRecipe) : CookingRecipeWrapper(recipe) { +class BlastingRecipeWrapper @JvmOverloads constructor( + recipe: BlastingRecipe, + recipeInput: RecipeInput.Item = recipe.inputChoice.asRecipeInput() +) : CookingRecipeWrapper(recipe, recipeInput) { override val displayBlock = Material.BLAST_FURNACE } -class CampfireRecipeWrapper(recipe: CampfireRecipe) : CookingRecipeWrapper(recipe) { +class CampfireRecipeWrapper @JvmOverloads constructor( + recipe: CampfireRecipe, + recipeInput: RecipeInput.Item = recipe.inputChoice.asRecipeInput() +) : CookingRecipeWrapper(recipe, recipeInput) { override val displayBlock = Material.CAMPFIRE } -class FurnaceRecipeWrapper(recipe: FurnaceRecipe) : CookingRecipeWrapper(recipe) { +class FurnaceRecipeWrapper @JvmOverloads constructor( + recipe: FurnaceRecipe, + recipeInput: RecipeInput.Item = recipe.inputChoice.asRecipeInput() +) : CookingRecipeWrapper(recipe, recipeInput) { override val displayBlock = Material.FURNACE } -class SmokingRecipeWrapper(recipe: SmokingRecipe) : CookingRecipeWrapper(recipe) { +class SmokingRecipeWrapper @JvmOverloads constructor( + recipe: SmokingRecipe, + recipeInput: RecipeInput.Item = recipe.inputChoice.asRecipeInput() +) : CookingRecipeWrapper(recipe, recipeInput) { override val displayBlock = Material.SMOKER } -private inline fun > loadCookingRecipe( +@Suppress("UnstableApiUsage") +private inline fun , U : CookingRecipeWrapper> loadCookingRecipe( key: NamespacedKey, config: ConfigSection, defaultCookingTime: Int, - cons: (NamespacedKey, ItemStack, RecipeChoice, Float, Int) -> T -): T { + recipeCons: (NamespacedKey, ItemStack, RecipeChoice, Float, Int) -> T, + wrapperCons: (T, RecipeInput.Item) -> U +): U { val cookingTime = config.get("cookingtime", ConfigAdapter.INTEGER, defaultCookingTime) val experience = config.get("experience", ConfigAdapter.FLOAT, 0f) val ingredient = config.getOrThrow("ingredient", ConfigAdapter.RECIPE_INPUT_ITEM) + if (ingredient.representativeItems.any{ it.hasData(DataComponentTypes.MAX_DAMAGE) }) { + ingredient.ignoreComponents.add(DataComponentTypes.DAMAGE) + } val result = config.getOrThrow("result", ConfigAdapter.ITEM_STACK) - val recipe = cons(key, result, ingredient.asRecipeChoice(), experience, cookingTime) + val recipe = recipeCons(key, result, ingredient.asRecipeChoice(), experience, cookingTime) config.get("category", ConfigAdapter.ENUM.from())?.let { recipe.category = it } config.get("group", ConfigAdapter.STRING)?.let { recipe.group = it } - return recipe + return wrapperCons(recipe, ingredient) } /** @@ -88,10 +110,12 @@ private inline fun > loadCookingRecipe( */ object BlastingRecipeType : VanillaRecipeType("blasting") { - fun addRecipe(recipe: BlastingRecipe) = super.addRecipe(BlastingRecipeWrapper(recipe)) + @JvmOverloads + fun addRecipe(recipe: BlastingRecipe, recipeInput: RecipeInput.Item = recipe.inputChoice.asRecipeInput()) = + super.addRecipe(BlastingRecipeWrapper(recipe, recipeInput)) override fun loadRecipe(key: NamespacedKey, section: ConfigSection) = - BlastingRecipeWrapper(loadCookingRecipe(key, section, 100, ::BlastingRecipe)) + loadCookingRecipe(key, section, 100, ::BlastingRecipe, ::BlastingRecipeWrapper) } /** @@ -102,10 +126,12 @@ object BlastingRecipeType : VanillaRecipeType("blasting") */ object CampfireRecipeType : VanillaRecipeType("campfire_cooking") { - fun addRecipe(recipe: CampfireRecipe) = super.addRecipe(CampfireRecipeWrapper(recipe)) + @JvmOverloads + fun addRecipe(recipe: CampfireRecipe, recipeInput: RecipeInput.Item = recipe.inputChoice.asRecipeInput()) = + super.addRecipe(CampfireRecipeWrapper(recipe, recipeInput)) override fun loadRecipe(key: NamespacedKey, section: ConfigSection) = - CampfireRecipeWrapper(loadCookingRecipe(key, section, 600, ::CampfireRecipe)) + loadCookingRecipe(key, section, 600, ::CampfireRecipe, ::CampfireRecipeWrapper) } /** @@ -113,10 +139,12 @@ object CampfireRecipeType : VanillaRecipeType("campfire_c */ object FurnaceRecipeType : VanillaRecipeType("smelting") { - fun addRecipe(recipe: FurnaceRecipe) = super.addRecipe(FurnaceRecipeWrapper(recipe)) + @JvmOverloads + fun addRecipe(recipe: FurnaceRecipe, recipeInput: RecipeInput.Item = recipe.inputChoice.asRecipeInput()) = + super.addRecipe(FurnaceRecipeWrapper(recipe, recipeInput)) override fun loadRecipe(key: NamespacedKey, section: ConfigSection) = - FurnaceRecipeWrapper(loadCookingRecipe(key, section, 200, ::FurnaceRecipe)) + loadCookingRecipe(key, section, 200, ::FurnaceRecipe, ::FurnaceRecipeWrapper) } /** @@ -124,8 +152,10 @@ object FurnaceRecipeType : VanillaRecipeType("smelting") { */ object SmokingRecipeType : VanillaRecipeType("smoking") { - fun addRecipe(recipe: SmokingRecipe) = super.addRecipe(SmokingRecipeWrapper(recipe)) + @JvmOverloads + fun addRecipe(recipe: SmokingRecipe, recipeInput: RecipeInput.Item = recipe.inputChoice.asRecipeInput()) = + super.addRecipe(SmokingRecipeWrapper(recipe, recipeInput)) override fun loadRecipe(key: NamespacedKey, section: ConfigSection) = - SmokingRecipeWrapper(loadCookingRecipe(key, section, 100, ::SmokingRecipe)) + loadCookingRecipe(key, section, 100, ::SmokingRecipe, ::SmokingRecipeWrapper) } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/vanilla/Crafting.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/vanilla/Crafting.kt index 3c8210f05..99e73107f 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/vanilla/Crafting.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/vanilla/Crafting.kt @@ -4,6 +4,7 @@ import io.github.pylonmc.rebar.config.ConfigSection import io.github.pylonmc.rebar.config.adapter.ConfigAdapter import io.github.pylonmc.rebar.guide.button.ItemButton import io.github.pylonmc.rebar.recipe.FluidOrItem +import io.github.pylonmc.rebar.recipe.RebarRecipe import io.github.pylonmc.rebar.recipe.RecipeInput import io.github.pylonmc.rebar.util.gui.GuiItems import org.bukkit.Material @@ -12,20 +13,24 @@ import org.bukkit.inventory.* import org.bukkit.inventory.recipe.CraftingBookCategory import xyz.xenondevs.invui.gui.Gui import xyz.xenondevs.invui.item.Item +import kotlin.text.asIterable -sealed class CraftingRecipeWrapper(val craftingRecipe: CraftingRecipe) : VanillaRecipeWrapper { +sealed class CraftingRecipeWrapper(val craftingRecipe: CraftingRecipe, val recipeInput: List) : VanillaRecipeWrapper { override fun getKey(): NamespacedKey = craftingRecipe.key override val results: List = listOf(FluidOrItem.of(craftingRecipe.result)) } -class ShapedRecipeWrapper(override val recipe: ShapedRecipe) : CraftingRecipeWrapper(recipe) { - override val inputs: List = - recipe.shape - .flatMap { it.asIterable() } - .mapNotNull { - recipe.choiceMap[it]?.asRecipeInput() - } +class ShapedRecipeWrapper( + override val recipe: ShapedRecipe, + recipeInput: List = recipe.shape + .flatMap { it.asIterable() } + .map { + recipe.choiceMap[it]?.asRecipeInput() ?: RecipeInput.EMPTY_ITEM + } +) : CraftingRecipeWrapper(recipe, recipeInput) { + override val inputs: List = recipeInput + override fun display(): Gui { val gui = Gui.builder() @@ -56,13 +61,24 @@ class ShapedRecipeWrapper(override val recipe: ShapedRecipe) : CraftingRecipeWra val character = recipe.shape[y][x] return ItemButton.from(recipe.choiceMap[character]) } -} -sealed class AShapelessRecipeWrapper(recipe: CraftingRecipe) : CraftingRecipeWrapper(recipe) { + fun matches(items: List): Boolean { + if (RebarRecipe.matchesShaped(items, recipeInput, 3, 3)) return true + val mirror = items.toMutableList() + while (mirror.size < 9) mirror.add(null) + mirror[2] = mirror[0].also{ mirror[0] = mirror[2] } + mirror[5] = mirror[3].also{ mirror[3] = mirror[5] } + mirror[8] = mirror[6].also{ mirror[6] = mirror[8] } + return RebarRecipe.matchesShaped(mirror, recipeInput, 3, 3) + } +} - protected abstract val choiceList: List +sealed class AShapelessRecipeWrapper( + recipe: CraftingRecipe, + recipeInput: List +) : CraftingRecipeWrapper(recipe, recipeInput) { - override val inputs: List by lazy { choiceList.filterNotNull().map(RecipeChoice::asRecipeInput) } + override val inputs: List = recipeInput override fun display() = Gui.builder() .setStructure( @@ -87,17 +103,23 @@ sealed class AShapelessRecipeWrapper(recipe: CraftingRecipe) : CraftingRecipeWra .build() private fun getDisplaySlot(index: Int): Item { - return ItemButton.from(choiceList.getOrNull(index)) + return ItemButton.from(recipeInput.getOrNull(index)) } -} -class ShapelessRecipeWrapper(override val recipe: ShapelessRecipe) : AShapelessRecipeWrapper(recipe) { - override val choiceList = recipe.choiceList + fun matches(items: List): Boolean { + return RebarRecipe.matchesShapeless(items, recipeInput) + } } -class TransmuteRecipeWrapper(override val recipe: TransmuteRecipe) : AShapelessRecipeWrapper(recipe) { - override val choiceList = listOf(recipe.input, recipe.material) -} +class ShapelessRecipeWrapper( + override val recipe: ShapelessRecipe, + recipeInput: List = recipe.choiceList.filterNotNull().map(RecipeChoice::asRecipeInput) +) : AShapelessRecipeWrapper(recipe, recipeInput) + +class TransmuteRecipeWrapper( + override val recipe: TransmuteRecipe, + recipeInput: List = listOf(recipe.input.asRecipeInput(), recipe.material.asRecipeInput()) +) : AShapelessRecipeWrapper(recipe, recipeInput) private val CRAFTING_BOOK_CATEGORY_ADAPTER = ConfigAdapter.ENUM.from() diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/vanilla/VanillaRecipeType.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/vanilla/VanillaRecipeType.kt index d13e1379a..091248d7d 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/vanilla/VanillaRecipeType.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/vanilla/VanillaRecipeType.kt @@ -48,7 +48,7 @@ sealed class VanillaRecipeType(key: String) : } @JvmSynthetic -internal fun RecipeChoice.asRecipeInput(): RecipeInput { +internal fun RecipeChoice.asRecipeInput(): RecipeInput.Item { return when (this) { is RecipeChoice.ExactChoice -> RecipeInput.Item( this.choices.mapTo(mutableSetOf()) { ItemTypeWrapper(it) }, @@ -66,5 +66,8 @@ internal fun RecipeChoice.asRecipeInput(): RecipeInput { @JvmSynthetic internal fun RecipeInput.Item.asRecipeChoice(): RecipeChoice { + if (ignoreComponents.isNotEmpty()) { + return RecipeChoice.MaterialChoice(items.map { it.createItemStack().type }) + } return RecipeChoice.ExactChoice(items.map { it.createItemStack().asQuantity(amount) }) } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/LRUCache.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/LRUCache.kt new file mode 100644 index 000000000..1c50c3650 --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/LRUCache.kt @@ -0,0 +1,9 @@ +package io.github.pylonmc.rebar.util + +class LRUCache(private val initialCapacity: Int) : + LinkedHashMap(initialCapacity, 0.75f, true) { + + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean { + return size > initialCapacity + } +} \ No newline at end of file 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 0d12c1aed..db29681f0 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 @@ -9,8 +9,8 @@ import io.github.pylonmc.rebar.config.Config import io.github.pylonmc.rebar.config.ConfigSection import io.github.pylonmc.rebar.config.ContributorConfig import io.github.pylonmc.rebar.config.adapter.ConfigAdapter -import io.github.pylonmc.rebar.item.RebarItem import io.github.pylonmc.rebar.i18n.customMiniMessage +import io.github.pylonmc.rebar.item.RebarItem import io.github.pylonmc.rebar.nms.NmsAccessor import io.github.pylonmc.rebar.registry.RebarRegistry import io.github.pylonmc.rebar.util.position.BlockPosition @@ -628,3 +628,11 @@ suspend fun delayTicks(ticks: Long) = delay(ticks * 50) */ @JvmSynthetic fun CoroutineContext.createChildContext(): CoroutineContext = this + Job(this[Job]) + +fun ItemStack.hashIgnoreAmount(): Int { + var hash = 1 + hash = hash * 31 + type.hashCode() + hash = hash * 31 + (durability.toInt() and 0xffff) + hash = hash * 31 + (if (hasItemMeta()) itemMeta.hashCode() else 0) + return hash +} \ No newline at end of file