-
Notifications
You must be signed in to change notification settings - Fork 15
Recipe matching system rewrite #715
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
df80743
670ab85
fc5d807
936424a
b023ff2
8e31927
fea5d6e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,12 +50,118 @@ interface RebarRecipe : Keyed { | |
|
|
||
| companion object { | ||
| private val priorities = MapMaker().weakKeys().makeMap<RebarRecipe, Double>() | ||
| private val cache = LRUCache<Int, RebarRecipe?>(1000) | ||
|
|
||
| @JvmStatic | ||
| var RebarRecipe.priority: Double | ||
| get() = priorities.getOrDefault(this, 0.0) | ||
| 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() } | ||
|
Comment on lines
+62
to
+69
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How would caching work in practice for Pylon recipes? Could you maybe demonstrate it by adding a cache to a few Pylon recipes so we can see what this looks like from a user perspective? |
||
|
|
||
| inline fun <reified T : RebarRecipe> searchRecipes(recipeType: RecipeType<out T>, hint: NamespacedKey?, hash: Int, pred: Predicate<T>): T? { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needs documenting |
||
| // 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 <reified T : RebarRecipe> searchRecipes(recipeType: RecipeType<out T>, hash: Int, pred: Predicate<T>): T? | ||
| = searchRecipes(recipeType, null, hash, pred) | ||
|
|
||
| fun hashShaped2D(items: Iterable<Iterable<ItemStack?>>): Int = | ||
| items.fold(1) { outerHash, row -> | ||
| row.fold(outerHash) { hash, item -> 31 * hash + (item?.hashIgnoreAmount() ?: 0) } | ||
| } | ||
|
|
||
| fun hashShaped(items: Iterable<ItemStack?>): Int = | ||
| items.fold(1) { hash, i -> 31 * hash + (i?.hashIgnoreAmount() ?: 0) } | ||
|
|
||
| fun hashShapeless2D(items: Iterable<Iterable<ItemStack?>>): Int = | ||
| items.sumOf { row -> row.sumOf { it?.hashIgnoreAmount() ?: 0 } } | ||
|
|
||
| fun hashShapeless(items: Iterable<ItemStack?>): Int = | ||
| items.sumOf { it?.hashIgnoreAmount() ?: 0 } | ||
|
|
||
| fun matchesShaped(items: List<ItemStack?>, recipeInput: List<RecipeInput.Item>, 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<ItemStack?>, recipeInput: List<RecipeInput.Item>): 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<Int>() | ||
| 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 | ||
| } | ||
|
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<ItemTypeWrapper>, val amount: Int) : RecipeInput { | ||
| data class Item(val items: MutableSet<ItemTypeWrapper>, val amount: Int, val ignoreComponents: MutableSet<DataComponentType>) : RecipeInput { | ||
| constructor(items: MutableSet<ItemTypeWrapper>, amount: Int) : this(items, amount, mutableSetOf()) | ||
| constructor(amount: Int, vararg items: ItemStack) : this(items.mapTo(mutableSetOf()) { ItemTypeWrapper(it) }, amount) | ||
| constructor(tag: Tag<ItemTypeWrapper>, 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 | ||
| } | ||
|
Comment on lines
+35
to
+42
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure how I feel about creating an itemstack just to get it garbage collected this many times potentially. Maybe we could have a cache or something so that there's just one immutable instance used or something but yeah.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. made it so rebar items match against the template item but the vanilla item is a record so i cant put a cache field, do you think its worth it to derecord it |
||
|
|
||
| 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) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot of the code in this file is incredibly cursed, e.g. the manual re-implementation of shaped crafting, searchRecipes (which I think is only used by the vanilla wrappers?), etc. I would much prefer to keep this file as clean as possible because it's an important public interface. Can we move this code closer to where it is being used to 1) clean up this file and 2) make it clearer why half of these methods exist? It took me a solid 10-15 minutes just to understand why we were manually re-implementing shaped crafting partially because of the indirection, having this logic in e.g. RecipeListener would be good