Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -10,14 +11,20 @@ import org.bukkit.inventory.ItemStack
sealed interface ItemTypeWrapper : Keyed {

fun createItemStack(): ItemStack
fun matchesWithoutData(item: ItemStack, excludeTypes: Set<DataComponentType>, ignoreCount: Boolean = false): Boolean
fun matchesWithoutData(item: ItemStack, excludeTypes: Set<DataComponentType>): Boolean = matchesWithoutData(item, excludeTypes, false)
fun isEmpty(): Boolean

/**
* The vanilla variant of [ItemTypeWrapper].
*/
@JvmRecord
data class Vanilla(val material: Material) : ItemTypeWrapper {
override fun createItemStack() = ItemStack(material)
override fun matchesWithoutData(item: ItemStack, excludeTypes: Set<DataComponentType>, ignoreCount: Boolean) = createItemStack().matchesWithoutData(item, excludeTypes, ignoreCount)
override fun getKey() = material.key
override fun isEmpty() = material.isAir()
override fun toString() = "ItemTypeWrapper.Vanilla(${material.key})"
}

/**
Expand All @@ -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<DataComponentType>, 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -74,6 +74,10 @@ class RebarItemSchema @JvmOverloads internal constructor(
return BlockStorage.setBlock(context.block.position, blockSchema, context)
}

@JvmOverloads
fun matchesWithoutData(item: ItemStack, excludeTypes: Set<DataComponentType>, ignoreCount: Boolean = false) =
template.matchesWithoutData(item, excludeTypes, ignoreCount)

override fun getKey(): NamespacedKey = key

override fun equals(other: Any?): Boolean = key == (other as? RebarItemSchema)?.key
Expand Down
110 changes: 110 additions & 0 deletions rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RebarRecipe.kt
Copy link
Copy Markdown
Contributor

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

Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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? {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
Expand Up @@ -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)

Expand All @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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
Expand All @@ -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)
Expand Down
Loading