From 4a6689d47621eae399d58f5017057bb269f70718 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:09:01 +0000 Subject: [PATCH 1/2] fix: stop food and pot duplication - Opening a chest copied one food over any "equal" food in the inventory, but equality ignored carve state and other per-item data, so a carved roast or sausage chain became a whole one again. The copy now happens only when the two stacks differ in nothing but aging. - Pots and crafting stations kept a reference to the furniture object from before a chunk reload. Taking an item handed it out from the old object while the live furniture still dropped it on break. Stale references are rebuilt from the live furniture and dropped when their chunk unloads. - The mixing bowl and frying pan took the ingredient from a converted copy after setItemInMainHand, so the held stack was never reduced. - The sausage maker checked for casing paper when the crank started but took it when it finished; it now checks again before making the chain. - The empty cup replaced whatever was held a tick after drinking; it now replaces the glass bottle in the slot the cup was drunk from. Adds MockBukkit and Mockito as test dependencies for the item tests. Co-Authored-By: Claude Opus 5.5 (1M context) --- pom.xml | 25 +++++ .../cooking/cooking/FryingReference.java | 3 +- .../cooking/cup/DrinkConsumeListener.java | 38 +++++--- .../cooking/manager/ConversionManager.java | 28 +++++- .../cooking/manager/CookingManager.java | 13 ++- .../cooking/manager/CraftingManager.java | 12 ++- .../cooking/mixing/MixingBowlHandler.java | 3 +- .../sausagemaker/SausageMakerHandler.java | 8 ++ .../cooking/utils/StationChunks.java | 27 ++++++ .../cooking/cup/EmptyCupReturnTest.java | 73 ++++++++++++++ .../cooking/manager/FoodStackMergeTest.java | 95 +++++++++++++++++++ .../cooking/utils/StationChunksTest.java | 41 ++++++++ 12 files changed, 348 insertions(+), 18 deletions(-) create mode 100644 src/main/java/net/tfminecraft/cooking/utils/StationChunks.java create mode 100644 src/test/java/net/tfminecraft/cooking/cup/EmptyCupReturnTest.java create mode 100644 src/test/java/net/tfminecraft/cooking/manager/FoodStackMergeTest.java create mode 100644 src/test/java/net/tfminecraft/cooking/utils/StationChunksTest.java diff --git a/pom.xml b/pom.xml index ff72b79..3c67c1c 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,32 @@ 2.1.0 + + + + + org.junit + junit-bom + 5.10.2 + pom + import + + + + + + org.mockbukkit.mockbukkit + mockbukkit-v1.21 + 4.95.0 + test + + + org.mockito + mockito-core + 5.20.0 + test + local gson diff --git a/src/main/java/net/tfminecraft/cooking/cooking/FryingReference.java b/src/main/java/net/tfminecraft/cooking/cooking/FryingReference.java index 1816206..0d3e5d1 100644 --- a/src/main/java/net/tfminecraft/cooking/cooking/FryingReference.java +++ b/src/main/java/net/tfminecraft/cooking/cooking/FryingReference.java @@ -106,7 +106,8 @@ public void interact(FurnitureInteractEvent e) { ItemStack converted = IngredientConverter.convertIfNeeded(p, item); if (converted != item) { p.getInventory().setItemInMainHand(converted); - item = converted; + // setItemInMainHand stores a copy, so take from the hand itself or nothing is used up. + item = p.getInventory().getItemInMainHand(); } FoodItem butterFi = FoodItem.fromItem(item); diff --git a/src/main/java/net/tfminecraft/cooking/cup/DrinkConsumeListener.java b/src/main/java/net/tfminecraft/cooking/cup/DrinkConsumeListener.java index 68bfbdf..1416cc3 100644 --- a/src/main/java/net/tfminecraft/cooking/cup/DrinkConsumeListener.java +++ b/src/main/java/net/tfminecraft/cooking/cup/DrinkConsumeListener.java @@ -1,5 +1,7 @@ package net.tfminecraft.cooking.cup; +import java.util.function.Supplier; + import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Material; @@ -66,24 +68,34 @@ public void onDrink(PlayerItemConsumeEvent event) { } int amountBefore = consumed.getAmount(); - Bukkit.getScheduler().runTask(Cooking.plugin, () -> replaceWithEmptyCup(player, amountBefore)); + EquipmentSlot hand = event.getHand() == EquipmentSlot.OFF_HAND ? EquipmentSlot.OFF_HAND : EquipmentSlot.HAND; + int slot = hand == EquipmentSlot.OFF_HAND ? -1 : player.getInventory().getHeldItemSlot(); + Bukkit.getScheduler().runTask(Cooking.plugin, () -> replaceWithEmptyCup(player, amountBefore, slot)); + } + + /** + * Swaps the glass bottle vanilla leaves in the slot the cup was drunk from for an empty cup. + * Only that slot, and only if it still holds the bottle: switching slots within the tick + * must neither leave the bottle and add a cup, nor overwrite whatever is held instead. + */ + static void replaceWithEmptyCup(Player player, int amountBefore, int slot) { + replaceWithEmptyCup(player, amountBefore, slot, CupItems::emptyCup); } - private static void replaceWithEmptyCup(Player player, int amountBefore) { + static void replaceWithEmptyCup(Player player, int amountBefore, int slot, Supplier emptyCup) { if (amountBefore > 1) { return; } - - ItemStack empty = CupItems.emptyCup(); - ItemStack main = player.getInventory().getItemInMainHand(); - ItemStack off = player.getInventory().getItemInOffHand(); - - if (main.getType() == Material.GLASS_BOTTLE || main.getType() == Material.AIR) { - player.getInventory().setItemInMainHand(empty); - } else if (off.getType() == Material.GLASS_BOTTLE) { - player.getInventory().setItemInOffHand(empty); - } else if (!ItemCache.isCupOfWater(main) && !ItemCache.isCupOfMilk(main)) { - player.getInventory().setItemInMainHand(empty); + PlayerInventory inv = player.getInventory(); + ItemStack left = slot < 0 ? inv.getItemInOffHand() : inv.getItem(slot); + if (left != null && left.getType() != Material.GLASS_BOTTLE && left.getType() != Material.AIR) { + return; + } + ItemStack empty = emptyCup.get(); + if (slot < 0) { + inv.setItemInOffHand(empty); + } else { + inv.setItem(slot, empty); } } diff --git a/src/main/java/net/tfminecraft/cooking/manager/ConversionManager.java b/src/main/java/net/tfminecraft/cooking/manager/ConversionManager.java index aa846f5..bf6df93 100644 --- a/src/main/java/net/tfminecraft/cooking/manager/ConversionManager.java +++ b/src/main/java/net/tfminecraft/cooking/manager/ConversionManager.java @@ -23,6 +23,7 @@ import net.tfminecraft.cooking.utils.FoodParser; import net.tfminecraft.cooking.utils.InventoryAdder; import net.tfminecraft.cooking.utils.ItemBuilder; +import net.tfminecraft.cooking.utils.Keys; public class ConversionManager implements Listener { @@ -132,7 +133,7 @@ public void onOpen(InventoryOpenEvent e) { FoodItem fb = FoodItem.fromItem(b); if (fb == null) continue; - if (InventoryAdder.equalsFood(fa, fb)) { + if (InventoryAdder.equalsFood(fa, fb) && sameApartFromAging(a, b)) { ItemStack clone = b.clone(); clone.setAmount(a.getAmount()); setSlot(i, clone, top, bottom, topSize); @@ -142,6 +143,31 @@ public void onOpen(InventoryOpenEvent e) { } } + /** + * True when two food stacks differ only in how far they have aged, so one can be copied over + * the other to let them stack. equalsFood matches by kind and tag step alone, so without this + * a carved roast, a sausage chain or a big catch would be replaced by a fresh copy of another. + */ + public static boolean sameApartFromAging(ItemStack a, ItemStack b) { + return withoutAging(a).isSimilar(withoutAging(b)); + } + + private static ItemStack withoutAging(ItemStack item) { + ItemStack copy = item.clone(); + copy.setAmount(1); + ItemMeta meta = copy.getItemMeta(); + if (meta == null) return copy; + var pdc = meta.getPersistentDataContainer(); + // The clock, the progress within each tag step, and the lore written from them. + pdc.remove(Keys.LAST_UPDATE); + pdc.remove(Keys.AGE_REMAINDER); + pdc.remove(Keys.TAGS); + pdc.remove(Keys.LORE_INDEX_MAP); + meta.lore(null); + copy.setItemMeta(meta); + return copy; + } + private ItemStack getSlot(int index, Inventory top, Inventory bottom, int topSize) { return index < topSize ? top.getItem(index) : bottom.getItem(index - topSize); } diff --git a/src/main/java/net/tfminecraft/cooking/manager/CookingManager.java b/src/main/java/net/tfminecraft/cooking/manager/CookingManager.java index 56403e8..f5901f0 100644 --- a/src/main/java/net/tfminecraft/cooking/manager/CookingManager.java +++ b/src/main/java/net/tfminecraft/cooking/manager/CookingManager.java @@ -13,6 +13,7 @@ import org.bukkit.event.block.Action; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.ChunkUnloadEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.scheduler.BukkitRunnable; @@ -27,6 +28,7 @@ import net.tfminecraft.cooking.cooking.SauceReference; import net.tfminecraft.cooking.enums.Method; import net.tfminecraft.cooking.item.FoodItem; +import net.tfminecraft.cooking.utils.StationChunks; import net.tfminecraft.interactiblefurniture.events.FurnitureBreakEvent; import net.tfminecraft.interactiblefurniture.events.FurnitureInteractEvent; import net.tfminecraft.interactiblefurniture.events.FurnitureSlotItemAddEvent; @@ -76,6 +78,13 @@ private void resumeStationsInChunk(Chunk chunk) { } } + /** Drops references to furniture in an unloading chunk; loading it again builds fresh ones. */ + @EventHandler + public void onChunkUnload(ChunkUnloadEvent event) { + Chunk chunk = event.getChunk(); + stations.values().removeIf(ref -> StationChunks.isIn(ref.getFurniture(), chunk)); + } + @EventHandler public void onChunkLoad(ChunkLoadEvent event) { Chunk chunk = event.getChunk(); @@ -97,7 +106,9 @@ private CookingReference createReference(Furniture furniture, Method method) { private CookingReference getOrCreateReference(Furniture furniture) { CookingReference existing = stations.get(furniture.getEntityId()); - if (existing != null) { + // A chunk reload replaces the furniture object under the same UUID. A reference still + // holding the old one would hand out items the live furniture also keeps, so rebuild it. + if (existing != null && existing.getFurniture() == furniture) { return existing; } Method method = FurnitureCache.getByFurniture(furniture); diff --git a/src/main/java/net/tfminecraft/cooking/manager/CraftingManager.java b/src/main/java/net/tfminecraft/cooking/manager/CraftingManager.java index 2847d0c..2d1d965 100644 --- a/src/main/java/net/tfminecraft/cooking/manager/CraftingManager.java +++ b/src/main/java/net/tfminecraft/cooking/manager/CraftingManager.java @@ -16,6 +16,7 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.ChunkUnloadEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.util.Transformation; @@ -34,6 +35,7 @@ import net.tfminecraft.cooking.item.FoodItem; import net.tfminecraft.cooking.item.data.CookData; import net.tfminecraft.cooking.utils.ItemUpdater; +import net.tfminecraft.cooking.utils.StationChunks; import net.tfminecraft.cooking.loader.CraftingStationLoader; import net.tfminecraft.interactiblefurniture.events.FurnitureBreakEvent; import net.tfminecraft.interactiblefurniture.events.FurnitureInteractEvent; @@ -92,7 +94,8 @@ private boolean isCraftingStationFurniture(Furniture f) { public CraftingStation getOrCreateStation(Furniture f) { if (f.getType() == null) return null; CraftingStation existing = stations.get(f.getEntityId()); - if (existing != null) return existing; + // A chunk reload replaces the furniture object under the same UUID; rebuild from the live one. + if (existing != null && existing.getFurniture() == f) return existing; for (CraftingStation template : CraftingStationLoader.get()) { if (template.getBlockId().equalsIgnoreCase(f.getType().getId())) { @@ -105,6 +108,13 @@ public CraftingStation getOrCreateStation(Furniture f) { return null; } + /** Drops stations in an unloading chunk; loading it again builds fresh ones. */ + @EventHandler + public void onChunkUnload(ChunkUnloadEvent event) { + Chunk chunk = event.getChunk(); + stations.values().removeIf(station -> StationChunks.isIn(station.getFurniture(), chunk)); + } + @EventHandler public void onChunkLoad(ChunkLoadEvent event) { Chunk chunk = event.getChunk(); diff --git a/src/main/java/net/tfminecraft/cooking/mixing/MixingBowlHandler.java b/src/main/java/net/tfminecraft/cooking/mixing/MixingBowlHandler.java index eb75656..9c94927 100644 --- a/src/main/java/net/tfminecraft/cooking/mixing/MixingBowlHandler.java +++ b/src/main/java/net/tfminecraft/cooking/mixing/MixingBowlHandler.java @@ -319,7 +319,8 @@ private void handleMixinAdd(Furniture furniture, Player player, FurnitureInterac ItemStack converted = IngredientConverter.convertIfNeeded(player, hand); if (converted != hand) { player.getInventory().setItemInMainHand(converted); - hand = converted; + // setItemInMainHand stores a copy, so take from the hand itself or nothing is used up. + hand = player.getInventory().getItemInMainHand(); } FoodItem foodItem = FoodItem.fromItem(hand); diff --git a/src/main/java/net/tfminecraft/cooking/sausagemaker/SausageMakerHandler.java b/src/main/java/net/tfminecraft/cooking/sausagemaker/SausageMakerHandler.java index 6264502..a53768a 100644 --- a/src/main/java/net/tfminecraft/cooking/sausagemaker/SausageMakerHandler.java +++ b/src/main/java/net/tfminecraft/cooking/sausagemaker/SausageMakerHandler.java @@ -136,10 +136,18 @@ private void startCrank(Furniture furniture, Player player) { } private void finishCrank(Furniture furniture, Player player) { + if (!player.isOnline()) { + return; + } List meats = collectMeats(furniture); if (meats.size() < MEAT_SLOTS.length) { return; } + // The paper was checked when the crank started; it could have been put away since. + if (!hasPaper(player)) { + player.sendMessage("§cHold paper to casing."); + return; + } ItemStack chain = SausageItems.fromMeats(player, meats); if (chain == null) { diff --git a/src/main/java/net/tfminecraft/cooking/utils/StationChunks.java b/src/main/java/net/tfminecraft/cooking/utils/StationChunks.java new file mode 100644 index 0000000..55cf733 --- /dev/null +++ b/src/main/java/net/tfminecraft/cooking/utils/StationChunks.java @@ -0,0 +1,27 @@ +package net.tfminecraft.cooking.utils; + +import org.bukkit.Chunk; +import org.bukkit.Location; + +import net.tfminecraft.interactiblefurniture.furniture.Furniture; + +/** Chunk checks for cached stations that never load a chunk to answer. */ +public final class StationChunks { + + private StationChunks() { + } + + /** True when the furniture sits in this chunk. A carried piece counts where it is now. */ + public static boolean isIn(Furniture furniture, Chunk chunk) { + if (furniture == null || chunk == null) return false; + Location loc = furniture.getLoc(); + return isIn(loc, chunk); + } + + static boolean isIn(Location loc, Chunk chunk) { + if (loc == null || loc.getWorld() == null || chunk.getWorld() == null) return false; + return loc.getWorld().getUID().equals(chunk.getWorld().getUID()) + && (loc.getBlockX() >> 4) == chunk.getX() + && (loc.getBlockZ() >> 4) == chunk.getZ(); + } +} diff --git a/src/test/java/net/tfminecraft/cooking/cup/EmptyCupReturnTest.java b/src/test/java/net/tfminecraft/cooking/cup/EmptyCupReturnTest.java new file mode 100644 index 0000000..0f33a73 --- /dev/null +++ b/src/test/java/net/tfminecraft/cooking/cup/EmptyCupReturnTest.java @@ -0,0 +1,73 @@ +package net.tfminecraft.cooking.cup; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.entity.PlayerMock; + +/** A drunk cup leaves a glass bottle in its slot, which becomes an empty cup there and nowhere else. */ +class EmptyCupReturnTest { + private PlayerMock player; + private PlayerInventory inventory; + + @BeforeEach + void setUp() { + player = MockBukkit.mock().addPlayer(); + inventory = player.getInventory(); + } + + @AfterEach + void tearDown() { + MockBukkit.unmock(); + } + + private static ItemStack cup() { + return new ItemStack(Material.BOWL); + } + + @Test + void theBottleInTheSlotDrunkFromBecomesACup() { + inventory.setItem(3, new ItemStack(Material.GLASS_BOTTLE)); + + DrinkConsumeListener.replaceWithEmptyCup(player, 1, 3, EmptyCupReturnTest::cup); + + assertEquals(Material.BOWL, inventory.getItem(3).getType()); + } + + @Test + void switchingSlotsDoesNotOverwriteTheNewItem() { + inventory.setItem(3, new ItemStack(Material.GLASS_BOTTLE)); + inventory.setItem(5, new ItemStack(Material.DIAMOND_SWORD)); + inventory.setHeldItemSlot(5); + + DrinkConsumeListener.replaceWithEmptyCup(player, 1, 3, EmptyCupReturnTest::cup); + + assertEquals(Material.BOWL, inventory.getItem(3).getType()); + assertEquals(Material.DIAMOND_SWORD, inventory.getItem(5).getType()); + } + + @Test + void aBottleMovedAwayGetsNoCupAsWell() { + inventory.setItem(3, new ItemStack(Material.BREAD)); + + DrinkConsumeListener.replaceWithEmptyCup(player, 1, 3, () -> fail("no cup should be made")); + + assertEquals(Material.BREAD, inventory.getItem(3).getType()); + } + + @Test + void theOffHandIsUsedWhenTheCupWasDrunkFromIt() { + inventory.setItemInOffHand(new ItemStack(Material.GLASS_BOTTLE)); + + DrinkConsumeListener.replaceWithEmptyCup(player, 1, -1, EmptyCupReturnTest::cup); + + assertEquals(Material.BOWL, inventory.getItemInOffHand().getType()); + } +} diff --git a/src/test/java/net/tfminecraft/cooking/manager/FoodStackMergeTest.java b/src/test/java/net/tfminecraft/cooking/manager/FoodStackMergeTest.java new file mode 100644 index 0000000..d2aae9b --- /dev/null +++ b/src/test/java/net/tfminecraft/cooking/manager/FoodStackMergeTest.java @@ -0,0 +1,95 @@ +package net.tfminecraft.cooking.manager; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; + +import net.kyori.adventure.text.Component; +import net.tfminecraft.cooking.Cooking; +import net.tfminecraft.cooking.utils.Keys; + +/** + * Opening a chest copies one food over another equal one so the two stack. Only the aging may + * differ; anything else, such as how far a roast has been carved, must stay with its own item. + */ +class FoodStackMergeTest { + private static Cooking previous; + + @BeforeAll + static void setUp() { + MockBukkit.mock(); + previous = Cooking.plugin; + Cooking plugin = mock(Cooking.class); + when(plugin.namespace()).thenReturn("cooking"); + when(plugin.getName()).thenReturn("Cooking"); + Cooking.plugin = plugin; + } + + @AfterAll + static void tearDown() { + Cooking.plugin = previous; + MockBukkit.unmock(); + } + + private static ItemStack roast(int amount, long lastUpdate, String tags, String lore) { + ItemStack item = new ItemStack(Material.COOKED_BEEF, amount); + ItemMeta meta = item.getItemMeta(); + var pdc = meta.getPersistentDataContainer(); + pdc.set(Keys.FOOD_ID, PersistentDataType.STRING, "beef_roast"); + pdc.set(Keys.LAST_UPDATE, PersistentDataType.LONG, lastUpdate); + pdc.set(Keys.TAGS, PersistentDataType.STRING, tags); + meta.lore(List.of(Component.text(lore))); + item.setItemMeta(meta); + return item; + } + + private static ItemStack with(ItemStack item, java.util.function.Consumer change) { + ItemStack copy = item.clone(); + ItemMeta meta = copy.getItemMeta(); + change.accept(meta); + copy.setItemMeta(meta); + return copy; + } + + @Test + void foodsThatOnlyAgedDifferentlyCanMerge() { + ItemStack older = roast(3, 1_000L, "cooked.3", "Fresh"); + ItemStack newer = roast(1, 9_000L, "cooked.7", "Fresh, 2 minutes"); + + assertTrue(ConversionManager.sameApartFromAging(older, newer)); + } + + @Test + void aCarvedRoastIsNotReplacedByAWholeOne() { + ItemStack whole = roast(1, 1_000L, "cooked.3", "Fresh"); + ItemStack carved = with(whole, m -> m.getPersistentDataContainer() + .set(Keys.CARVE_REMAINING, PersistentDataType.INTEGER, 2)); + ItemStack full = with(whole, m -> m.getPersistentDataContainer() + .set(Keys.CARVE_REMAINING, PersistentDataType.INTEGER, 8)); + + assertFalse(ConversionManager.sameApartFromAging(carved, full)); + } + + @Test + void anyOtherDifferenceKeepsTheItemsApart() { + ItemStack plain = roast(1, 1_000L, "cooked.3", "Fresh"); + + assertFalse(ConversionManager.sameApartFromAging(plain, with(plain, m -> m.getPersistentDataContainer() + .set(Keys.BASE_FOOD, PersistentDataType.DOUBLE, 12.0)))); + assertFalse(ConversionManager.sameApartFromAging(plain, with(plain, m -> m.getPersistentDataContainer() + .set(Keys.CATCH_SIZE_CM, PersistentDataType.INTEGER, 90)))); + assertFalse(ConversionManager.sameApartFromAging(plain, new ItemStack(Material.COOKED_PORKCHOP))); + } +} diff --git a/src/test/java/net/tfminecraft/cooking/utils/StationChunksTest.java b/src/test/java/net/tfminecraft/cooking/utils/StationChunksTest.java new file mode 100644 index 0000000..ac3ecbc --- /dev/null +++ b/src/test/java/net/tfminecraft/cooking/utils/StationChunksTest.java @@ -0,0 +1,41 @@ +package net.tfminecraft.cooking.utils; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; + +class StationChunksTest { + private ServerMock server; + + @BeforeEach + void setUp() { + server = MockBukkit.mock(); + } + + @AfterEach + void tearDown() { + MockBukkit.unmock(); + } + + @Test + void findsFurnitureInTheUnloadingChunkOnly() { + World world = server.addSimpleWorld("world"); + World other = server.addSimpleWorld("other"); + Chunk chunk = world.getChunkAt(1, -2); + + assertTrue(StationChunks.isIn(new Location(world, 16, 64, -32), chunk)); + assertTrue(StationChunks.isIn(new Location(world, 31.9, 64, -17.5), chunk)); + assertFalse(StationChunks.isIn(new Location(world, 32, 64, -32), chunk)); + assertFalse(StationChunks.isIn(new Location(world, 16, 64, -33), chunk)); + assertFalse(StationChunks.isIn(new Location(other, 16, 64, -32), chunk)); + assertFalse(StationChunks.isIn((Location) null, chunk)); + } +} From 1a85ed97fecb77eac4c868d35220197f1ffea169 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:21:01 +0000 Subject: [PATCH 2/2] fix: return a cup only where the glass bottle still is Co-Authored-By: Claude Opus 5.5 (1M context) --- .../net/tfminecraft/cooking/cup/DrinkConsumeListener.java | 5 +++-- .../net/tfminecraft/cooking/manager/ConversionManager.java | 3 ++- .../net/tfminecraft/cooking/cup/EmptyCupReturnTest.java | 7 +++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/tfminecraft/cooking/cup/DrinkConsumeListener.java b/src/main/java/net/tfminecraft/cooking/cup/DrinkConsumeListener.java index 1416cc3..742247a 100644 --- a/src/main/java/net/tfminecraft/cooking/cup/DrinkConsumeListener.java +++ b/src/main/java/net/tfminecraft/cooking/cup/DrinkConsumeListener.java @@ -76,7 +76,8 @@ public void onDrink(PlayerItemConsumeEvent event) { /** * Swaps the glass bottle vanilla leaves in the slot the cup was drunk from for an empty cup. * Only that slot, and only if it still holds the bottle: switching slots within the tick - * must neither leave the bottle and add a cup, nor overwrite whatever is held instead. + * must neither leave the bottle and add a cup, nor overwrite whatever is held instead. Cups + * are potions, so an empty slot means the bottle was moved away, or nothing was used up. */ static void replaceWithEmptyCup(Player player, int amountBefore, int slot) { replaceWithEmptyCup(player, amountBefore, slot, CupItems::emptyCup); @@ -88,7 +89,7 @@ static void replaceWithEmptyCup(Player player, int amountBefore, int slot, Suppl } PlayerInventory inv = player.getInventory(); ItemStack left = slot < 0 ? inv.getItemInOffHand() : inv.getItem(slot); - if (left != null && left.getType() != Material.GLASS_BOTTLE && left.getType() != Material.AIR) { + if (left == null || left.getType() != Material.GLASS_BOTTLE) { return; } ItemStack empty = emptyCup.get(); diff --git a/src/main/java/net/tfminecraft/cooking/manager/ConversionManager.java b/src/main/java/net/tfminecraft/cooking/manager/ConversionManager.java index bf6df93..f44688b 100644 --- a/src/main/java/net/tfminecraft/cooking/manager/ConversionManager.java +++ b/src/main/java/net/tfminecraft/cooking/manager/ConversionManager.java @@ -158,7 +158,8 @@ private static ItemStack withoutAging(ItemStack item) { ItemMeta meta = copy.getItemMeta(); if (meta == null) return copy; var pdc = meta.getPersistentDataContainer(); - // The clock, the progress within each tag step, and the lore written from them. + // The clock, the progress within each tag step, and the lore written from them. Food lore + // is always rebuilt whole from the item's data (ItemBuilder.stamp), which is compared. pdc.remove(Keys.LAST_UPDATE); pdc.remove(Keys.AGE_REMAINDER); pdc.remove(Keys.TAGS); diff --git a/src/test/java/net/tfminecraft/cooking/cup/EmptyCupReturnTest.java b/src/test/java/net/tfminecraft/cooking/cup/EmptyCupReturnTest.java index 0f33a73..8e668ba 100644 --- a/src/test/java/net/tfminecraft/cooking/cup/EmptyCupReturnTest.java +++ b/src/test/java/net/tfminecraft/cooking/cup/EmptyCupReturnTest.java @@ -62,6 +62,13 @@ void aBottleMovedAwayGetsNoCupAsWell() { assertEquals(Material.BREAD, inventory.getItem(3).getType()); } + @Test + void aSlotEmptiedOfItsBottleGetsNoCup() { + DrinkConsumeListener.replaceWithEmptyCup(player, 1, 3, () -> fail("no cup should be made")); + + assertEquals(Material.AIR, inventory.getItem(3) == null ? Material.AIR : inventory.getItem(3).getType()); + } + @Test void theOffHandIsUsedWhenTheCupWasDrunkFromIt() { inventory.setItemInOffHand(new ItemStack(Material.GLASS_BOTTLE));