Skip to content

Commit 3669679

Browse files
authored
Merge pull request #770 from pylonmc/human/furnace-cache-fix
Furance Recipe Cache Fix
2 parents e07757a + 4f03229 commit 3669679

5 files changed

Lines changed: 164 additions & 4 deletions

File tree

nms/src/main/kotlin/io/github/pylonmc/rebar/nms/NmsAccessorImpl.kt

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import io.github.pylonmc.rebar.entity.packet.BlockTextureEntity
88
import io.github.pylonmc.rebar.i18n.PlayerTranslationHandler
99
import io.github.pylonmc.rebar.i18n.packet.PlayerPacketHandler
1010
import io.github.pylonmc.rebar.nms.entity.BlockTextureEntityImpl
11+
import io.github.pylonmc.rebar.nms.recipe.AccessibleCachedCheck
1112
import io.github.pylonmc.rebar.nms.recipe.HandlerRecipeBookClick
1213
import io.github.pylonmc.rebar.util.position.BlockPosition
1314
import io.papermc.paper.adventure.PaperAdventure
@@ -25,14 +26,16 @@ import net.minecraft.server.MinecraftServer
2526
import net.minecraft.world.inventory.AbstractCraftingMenu
2627
import net.minecraft.world.inventory.RecipeBookMenu.PostPlaceAction
2728
import net.minecraft.world.item.Item
29+
import net.minecraft.world.item.crafting.RecipeManager
30+
import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity
2831
import net.minecraft.world.level.block.state.properties.Property
2932
import org.bukkit.Material
33+
import org.bukkit.NamespacedKey
3034
import org.bukkit.World
3135
import org.bukkit.block.Block
3236
import org.bukkit.craftbukkit.CraftEquipmentSlot
3337
import org.bukkit.craftbukkit.CraftWorld
3438
import org.bukkit.craftbukkit.block.CraftBlock
35-
import org.bukkit.craftbukkit.entity.CraftEntity
3639
import org.bukkit.craftbukkit.entity.CraftLivingEntity
3740
import org.bukkit.craftbukkit.entity.CraftPlayer
3841
import org.bukkit.craftbukkit.inventory.CraftItemStack
@@ -45,6 +48,10 @@ import org.bukkit.entity.Player
4548
import org.bukkit.inventory.EquipmentSlot
4649
import org.bukkit.inventory.ItemStack
4750
import org.bukkit.persistence.PersistentDataContainer
51+
import java.lang.invoke.MethodHandle
52+
import java.lang.invoke.MethodHandles
53+
import java.lang.invoke.VarHandle
54+
import java.lang.reflect.Field
4855
import java.util.UUID
4956
import java.util.concurrent.ConcurrentHashMap
5057
import kotlin.coroutines.EmptyCoroutineContext
@@ -56,6 +63,25 @@ import net.minecraft.world.entity.EquipmentSlot as NmsEquipmentSlot
5663
@Suppress("unused")
5764
object NmsAccessorImpl : NmsAccessor {
5865

66+
// We use both the field and the handle because the handle will have significantly better performance
67+
// getting the field value but cannot be used for setting so we still need the raw field.
68+
// (even if we used a VarHandle, because the field is normally final, setting will not work)
69+
private val furnaceQuickCheckField: Field
70+
private val furnaceQuickCheckHandle: MethodHandle
71+
72+
init {
73+
try {
74+
furnaceQuickCheckField = AbstractFurnaceBlockEntity::class.java.getDeclaredField("quickCheck")
75+
furnaceQuickCheckField.isAccessible = true
76+
77+
val methodHandles = MethodHandles.privateLookupIn(AbstractFurnaceBlockEntity::class.java, MethodHandles.lookup())
78+
furnaceQuickCheckHandle = methodHandles.unreflectGetter(furnaceQuickCheckField)
79+
} catch (e: Throwable) {
80+
Rebar.logger.severe("Failed to access furnace quick check: ${e.message}")
81+
throw RuntimeException(e)
82+
}
83+
}
84+
5985
private val players = ConcurrentHashMap<UUID, PlayerPacketHandler>()
6086

6187
override fun damageItem(itemStack: ItemStack, amount: Int, world: World, onBreak: (Material) -> Unit, force: Boolean) {
@@ -199,4 +225,22 @@ object NmsAccessorImpl : NmsAccessor {
199225
}
200226
blocks
201227
}
228+
229+
override fun setFurnaceRecipeCache(block: Block, recipe: NamespacedKey) {
230+
val block = block as CraftBlock
231+
val blockEntity = block.level.getBlockEntity(block.position) as? AbstractFurnaceBlockEntity ?: return
232+
try {
233+
val currentQuickCheck = furnaceQuickCheckHandle.invoke(blockEntity) as? RecipeManager.CachedCheck<*, *> ?: return
234+
if (currentQuickCheck is AccessibleCachedCheck<*, *>) {
235+
currentQuickCheck.lastRecipe = CraftNamespacedKey.toResourceKey(Registries.RECIPE, recipe)
236+
} else {
237+
val newQuickCheck = AccessibleCachedCheck(blockEntity.recipeType)
238+
newQuickCheck.lastRecipe = CraftNamespacedKey.toResourceKey(Registries.RECIPE, recipe)
239+
furnaceQuickCheckField.set(blockEntity, newQuickCheck)
240+
}
241+
} catch (e: Throwable) {
242+
Rebar.logger.severe("Failed to set furnace recipe cache: ${e.message}")
243+
e.printStackTrace()
244+
}
245+
}
202246
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.github.pylonmc.rebar.nms.recipe
2+
3+
import net.minecraft.resources.ResourceKey
4+
import net.minecraft.server.level.ServerLevel
5+
import net.minecraft.world.item.crafting.Recipe
6+
import net.minecraft.world.item.crafting.RecipeHolder
7+
import net.minecraft.world.item.crafting.RecipeInput
8+
import net.minecraft.world.item.crafting.RecipeManager
9+
import net.minecraft.world.item.crafting.RecipeType
10+
import java.util.Optional
11+
12+
/**
13+
* A dedicated class for [RecipeManager.CachedCheck] that allows changing the lastRecipe field
14+
* This is effectively identical to the result of [RecipeManager.createCheck] except it's not
15+
* an anonymous class
16+
*
17+
* This is necessary to avoid using laggy reflection on the anonymous class provided by [RecipeManager.createCheck]
18+
*/
19+
class AccessibleCachedCheck<I : RecipeInput, T : Recipe<I>>(
20+
val type: RecipeType<T>,
21+
var lastRecipe: ResourceKey<Recipe<*>>? = null
22+
) : RecipeManager.CachedCheck<I, T> {
23+
24+
override fun getRecipeFor(input: I, level: ServerLevel): Optional<RecipeHolder<T>> {
25+
val recipeManager = level.recipeAccess()
26+
val result = recipeManager.getRecipeFor(type, input, level, this.lastRecipe)
27+
if (result.isPresent) {
28+
this.lastRecipe = result.get().id
29+
}
30+
return result
31+
}
32+
33+
}

rebar/src/main/kotlin/io/github/pylonmc/rebar/nms/NmsAccessor.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import io.github.pylonmc.rebar.i18n.PlayerTranslationHandler
77
import io.github.pylonmc.rebar.util.position.BlockPosition
88
import net.kyori.adventure.text.Component
99
import org.bukkit.Material
10+
import org.bukkit.NamespacedKey
1011
import org.bukkit.World
1112
import org.bukkit.block.Block
1213
import org.bukkit.entity.Entity
@@ -57,6 +58,28 @@ interface NmsAccessor {
5758

5859
fun blocksBetween(from: BlockPosition, to: BlockPosition): List<Block>
5960

61+
/**
62+
* Furnaces have a recipe cache of the last recipe smelted, this is great
63+
* for performance but has an unexpected side effect once rebar items are introduced.
64+
*
65+
* Lets say you have a furnace with nothing cached and you put in a raw tin (rebar item) to smelt.
66+
* The furnace is going to search for a recipe that matches and there are 2 possible choices
67+
* 1. the raw tin -> tin ingot recipe, supplied by rebar using exact choice
68+
* 2. the raw iron -> iron ingot recipe, supplied by Minecraft, using material choice, so it validates even if the item is a rebar item
69+
*
70+
* Even if it picks the vanilla recipe, when it starts smelting & finishes smelting, rebar will ensure
71+
* the rebar recipe is used instead.
72+
*
73+
* However, the furnace itself has no way of knowing that we changed which recipe was used, so if the vanilla recipe was used
74+
* it will cache the vanilla recipe. Then, when it tries to start smelting another raw tin, it will check that it can fit
75+
* the result of the next valid recipe, which is evaluated to the cached vanilla recipe, and it will find that it can't, because a tin ingot
76+
* is in the result slot, not the iron ingot. This then deadlocks the furnace in a feedback loop of use the vanilla recipe, oh can't fit it.
77+
*
78+
* In order to avoid this, whenever we override the recipe being used, we set the recipe in the recipe cache so that next time
79+
* the furnace smelts, it uses the correct recipe and doesn't get deadlocked.
80+
*/
81+
fun setFurnaceRecipeCache(block: Block, recipe: NamespacedKey)
82+
6083
companion object {
6184
val instance = Class.forName("io.github.pylonmc.rebar.nms.NmsAccessorImpl")
6285
.getDeclaredField("INSTANCE")

rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/RecipeListener.kt

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import io.github.pylonmc.rebar.item.RebarItem
44
import io.github.pylonmc.rebar.item.RebarItemSchema
55
import io.github.pylonmc.rebar.item.base.*
66
import io.github.pylonmc.rebar.item.research.Research.Companion.canCraft
7+
import io.github.pylonmc.rebar.nms.NmsAccessor
78
import io.github.pylonmc.rebar.recipe.vanilla.CookingRecipeWrapper
89
import io.github.pylonmc.rebar.recipe.vanilla.ShapedRecipeType
910
import io.github.pylonmc.rebar.recipe.vanilla.VanillaRecipeType
11+
import io.github.pylonmc.rebar.recipe.vanilla.recipeType
1012
import io.github.pylonmc.rebar.util.isRebarAndIsNot
1113
import io.github.pylonmc.rebar.util.plainText
1214
import io.papermc.paper.datacomponent.DataComponentTypes
@@ -189,13 +191,55 @@ internal object RebarRecipeListener : Listener {
189191
}
190192
}
191193

194+
@EventHandler(priority = EventPriority.LOWEST)
195+
private fun onStartCook(e: FurnaceStartSmeltEvent) {
196+
if (RebarItemSchema.fromStack(e.source) == null) return
197+
198+
val originalRecipe = e.recipe
199+
if (originalRecipe.key !in VanillaRecipeType.nonRebarRecipes) {
200+
return
201+
}
202+
203+
val originalType = originalRecipe.recipeType
204+
if (originalType == null) {
205+
e.totalCookTime = 0 // instantly complete so that it doesn't show progress bar, this will get canceled in BlockCookEvent
206+
return
207+
}
208+
209+
for (recipe in originalType.recipes) {
210+
if (recipe is CookingRecipeWrapper && recipe.key !in VanillaRecipeType.nonRebarRecipes && recipe.recipe.inputChoice.test(e.source)) {
211+
e.totalCookTime = recipe.recipe.cookingTime
212+
NmsAccessor.instance.setFurnaceRecipeCache(e.block, recipe.key)
213+
break
214+
}
215+
}
216+
}
217+
192218
@EventHandler(priority = EventPriority.LOWEST)
193219
private fun onCook(e: BlockCookEvent) {
194220
if (RebarItemSchema.fromStack(e.source) == null) return
195221

196-
for (recipe in RecipeType.vanillaCookingRecipes()) {
197-
if (recipe.key !in VanillaRecipeType.nonRebarRecipes && recipe.recipe.inputChoice.test(e.source)) {
198-
e.result = recipe.recipe.result.clone()
222+
val originalRecipe = e.recipe
223+
if (originalRecipe == null) {
224+
e.isCancelled = true
225+
return
226+
}
227+
228+
if (originalRecipe.key !in VanillaRecipeType.nonRebarRecipes) {
229+
// already handled correctly
230+
return
231+
}
232+
233+
val originalType = originalRecipe.recipeType
234+
if (originalType == null) {
235+
e.isCancelled = true
236+
return
237+
}
238+
239+
for (recipe in originalType.recipes) {
240+
if (recipe is CookingRecipeWrapper && recipe.key !in VanillaRecipeType.nonRebarRecipes && recipe.recipe.inputChoice.test(e.source)) {
241+
e.result = recipe.recipe.result
242+
NmsAccessor.instance.setFurnaceRecipeCache(e.block, recipe.key)
199243
break
200244
}
201245
}

rebar/src/main/kotlin/io/github/pylonmc/rebar/recipe/vanilla/VanillaRecipeType.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ import io.github.pylonmc.rebar.item.ItemTypeWrapper
55
import io.github.pylonmc.rebar.recipe.ConfigurableRecipeType
66
import io.github.pylonmc.rebar.recipe.RebarRecipe
77
import io.github.pylonmc.rebar.recipe.RecipeInput
8+
import io.github.pylonmc.rebar.recipe.RecipeType
89
import org.bukkit.Bukkit
910
import org.bukkit.NamespacedKey
1011
import org.bukkit.event.Listener
12+
import org.bukkit.inventory.BlastingRecipe
13+
import org.bukkit.inventory.CampfireRecipe
14+
import org.bukkit.inventory.CookingRecipe
15+
import org.bukkit.inventory.FurnaceRecipe
1116
import org.bukkit.inventory.Recipe
1217
import org.bukkit.inventory.RecipeChoice
18+
import org.bukkit.inventory.SmokingRecipe
1319

1420
sealed interface VanillaRecipeWrapper : RebarRecipe {
1521
val recipe: Recipe
@@ -68,3 +74,13 @@ internal fun RecipeChoice.asRecipeInput(): RecipeInput {
6874
internal fun RecipeInput.Item.asRecipeChoice(): RecipeChoice {
6975
return RecipeChoice.ExactChoice(items.map { it.createItemStack().asQuantity(amount) })
7076
}
77+
78+
@get:JvmSynthetic
79+
val CookingRecipe<*>.recipeType: RecipeType<*>?
80+
get() = when (this) {
81+
is BlastingRecipe -> BlastingRecipeType
82+
is CampfireRecipe -> CampfireRecipeType
83+
is FurnaceRecipe -> FurnaceRecipeType
84+
is SmokingRecipe -> SmokingRecipeType
85+
else -> null
86+
}

0 commit comments

Comments
 (0)