Skip to content

Recipe matching system rewrite#715

Open
SchnTgaiSpock wants to merge 7 commits into
pylonmc:masterfrom
SchnTgaiSpock:feat/recipe-rewrite
Open

Recipe matching system rewrite#715
SchnTgaiSpock wants to merge 7 commits into
pylonmc:masterfrom
SchnTgaiSpock:feat/recipe-rewrite

Conversation

@SchnTgaiSpock
Copy link
Copy Markdown

This was originally supposed to be the companion to pylonmc/pylon#962 to let damaged items be smelted, but then i decided to make it more general for any component, and now i wanna just overhaul the recipe matching in RecipeListener

As of writing, this branch works with pylonmc/pylon#962

Also this is my first time writing kotlin so apologies if its absolute dogsh*t

@SchnTgaiSpock SchnTgaiSpock marked this pull request as draft April 28, 2026 08:35
Comment on lines +33 to +40
operator fun contains(itemStack: ItemStack): Boolean {
for (item in items) {
if (item.createItemStack().matchesWithoutData(itemStack, ignoreComponents, true)) {
return true
}
}
return false
}
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

@SchnTgaiSpock
Copy link
Copy Markdown
Author

okayyy so, slight problem, since steel boots and palladium boots both use diamond boots, mc gets the recipe (diamond_boots -> palladium_nugget) so when mc thinks steel boots smelt into platinum nuggets and wont even bother calling fuel burn or start smelt events (which is where we would normally correct it) if theres something in the output
image

@SchnTgaiSpock
Copy link
Copy Markdown
Author

you can never hate mojang enough bro

@SchnTgaiSpock SchnTgaiSpock marked this pull request as ready for review May 6, 2026 02:51
@SchnTgaiSpock
Copy link
Copy Markdown
Author

everythings ready for review, the thing above i think is a minor enough edge case ithink its fine to leave in cuz i genuinely dont know what to do with it besides ticking the furnace itself

Copy link
Copy Markdown
Contributor

@LordIdra LordIdra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +87 to 106
@Suppress("UnstableApiUsage")
private inline fun <T : CookingRecipe<T>, 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<CookingBookCategory>())?.let { recipe.category = it }
config.get("group", ConfigAdapter.STRING)?.let { recipe.group = it }
return recipe
return wrapperCons(recipe, ingredient)
}
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.

Could we make this not generic (i.e. not take in the constructors as parameters)? This is extremely hard to read as-is, I think it would be a lot more readable split up even though it's more code


sealed class CookingRecipeWrapper(final override val recipe: CookingRecipe<*>) : VanillaRecipeWrapper {
override val inputs: List<RecipeInput> = listOf(recipe.inputChoice.asRecipeInput())
sealed class CookingRecipeWrapper(final override val recipe: CookingRecipe<*>, val recipeInput: RecipeInput.Item) : VanillaRecipeWrapper {
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.

What's the purpose of the new recipeInput parameter? I can't find where it's used if anywhere?

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.

its used in the matches method which needs the RecipeInput.Item instead of RecipeChoice to properly match rebar items w/out durability (or any other component) for reasons outlined here pylonmc/pylon#962 (comment)

Comment on lines +65 to +73
fun matches(items: List<ItemStack?>): 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)
}
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.

I believe that shaped recipes are automatically mirrored already when added to the server. Or is this because we are manually reimplementing matchesShaped? If so, this should really be in matchesShaped, not in here

Comment on lines +3 to +9
class LRUCache<K, V>(private val initialCapacity: Int) :
LinkedHashMap<K, V>(initialCapacity, 0.75f, true) {

override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>?): Boolean {
return size > initialCapacity
}
} No newline at end of file
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 and maybe should also be internal

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

}
fun clearCache() { cache.clear() }

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

Comment on lines +62 to +69
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() }
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?

@LordIdra
Copy link
Copy Markdown
Contributor

LordIdra commented May 10, 2026

Another thing I should flag, recipe autocompletion from the crafting book is very likely to be broken by these changes and will need testing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants