diff --git a/src/main/java/net/tfminecraft/cooking/Cooking.java b/src/main/java/net/tfminecraft/cooking/Cooking.java index bee9313..0c072b8 100644 --- a/src/main/java/net/tfminecraft/cooking/Cooking.java +++ b/src/main/java/net/tfminecraft/cooking/Cooking.java @@ -43,6 +43,7 @@ import net.tfminecraft.cooking.loader.PermissionEffectsLoader; import net.tfminecraft.cooking.loader.CompositionConfigLoader; import net.tfminecraft.cooking.loader.QualityConfigLoader; +import net.tfminecraft.cooking.manager.BundleFoodStacker; import net.tfminecraft.cooking.manager.CommandManager; import net.tfminecraft.cooking.manager.ConversionManager; import net.tfminecraft.cooking.manager.CookingManager; @@ -262,6 +263,7 @@ public void registerListeners() { getServer().getPluginManager().registerEvents(new HusbandryMountListener(), this); getServer().getPluginManager().registerEvents(new HusbandryDamageListener(), this); getServer().getPluginManager().registerEvents(new ConversionManager(), this); + getServer().getPluginManager().registerEvents(new BundleFoodStacker(), this); getServer().getPluginManager().registerEvents(new RegenBlocker(), this); getServer().getPluginManager().registerEvents(new SaturationGuard(), this); getServer().getPluginManager().registerEvents(new NutritionLifecycleListener(), this); diff --git a/src/main/java/net/tfminecraft/cooking/liquid/LiquidContainerState.java b/src/main/java/net/tfminecraft/cooking/liquid/LiquidContainerState.java index ec380ec..823087c 100644 --- a/src/main/java/net/tfminecraft/cooking/liquid/LiquidContainerState.java +++ b/src/main/java/net/tfminecraft/cooking/liquid/LiquidContainerState.java @@ -114,10 +114,13 @@ public static void touchLastUpdate(Furniture furniture) { } public static void tickAge(Furniture furniture) { + tickAge(furniture, System.currentTimeMillis()); + } + + static void tickAge(Furniture furniture, long now) { if (!TYPE_MILK.equalsIgnoreCase(getType(furniture)) || isEmpty(furniture)) { return; } - long now = System.currentTimeMillis(); long last = getLastUpdate(furniture); long elapsedMs = now - last; if (elapsedMs <= 0) { diff --git a/src/main/java/net/tfminecraft/cooking/manager/BundleFoodStacker.java b/src/main/java/net/tfminecraft/cooking/manager/BundleFoodStacker.java new file mode 100644 index 0000000..089363e --- /dev/null +++ b/src/main/java/net/tfminecraft/cooking/manager/BundleFoodStacker.java @@ -0,0 +1,149 @@ +package net.tfminecraft.cooking.manager; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.bukkit.Bukkit; +import org.bukkit.Tag; +import org.bukkit.entity.HumanEntity; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BundleMeta; + +import net.tfminecraft.cooking.Cooking; +import net.tfminecraft.cooking.item.FoodItem; +import net.tfminecraft.cooking.item.tag.TagTrack; +import net.tfminecraft.cooking.utils.InventoryAdder; + +/** + * Aging foods carry their own clock, so two pieces of the same meat never match exactly and a + * bundle keeps them apart. After a click that may have put food into a bundle, equal foods in it + * are merged the way inventories already stack them. Each merge keeps the more aged copy's data, + * so putting old food in with fresh food can never make it fresher. + */ +public class BundleFoodStacker implements Listener { + + private static final String FRESHNESS = "freshness"; + + private final Consumer nextTick; + + public BundleFoodStacker() { + this(task -> Bukkit.getScheduler().runTask(Cooking.plugin, task)); + } + + BundleFoodStacker(Consumer nextTick) { + this.nextTick = nextTick; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onClick(InventoryClickEvent e) { + if (!isBundle(e.getCursor()) && !isBundle(e.getCurrentItem())) return; + + HumanEntity player = e.getWhoClicked(); + Inventory clicked = e.getClickedInventory(); + int slot = e.getSlot(); + // The bundle changes after this event, so merge on the next tick. Read the cursor and the + // slot again then: the player may have moved things in between. + nextTick.accept(() -> stackBundles(player, clicked, slot)); + } + + static void stackBundles(HumanEntity player, Inventory clicked, int slot) { + ItemStack cursor = stack(player.getItemOnCursor()); + if (cursor != null) player.setItemOnCursor(cursor); + + if (clicked == null || !inView(player.getOpenInventory(), clicked)) return; + ItemStack inSlot = stack(clicked.getItem(slot)); + if (inSlot != null) clicked.setItem(slot, inSlot); + } + + private static boolean inView(InventoryView view, Inventory inv) { + return view.getTopInventory() == inv || view.getBottomInventory() == inv; + } + + /** The bundle with its foods merged, or null when it is not a bundle or nothing merged. */ + static ItemStack stack(ItemStack bundle) { + if (!isBundle(bundle) || !(bundle.getItemMeta() instanceof BundleMeta meta)) return null; + List merged = merge(meta.getItems()); + if (merged == null) return null; + + ItemStack out = bundle.clone(); + meta.setItems(merged); + out.setItemMeta(meta); + return out; + } + + /** Bundle contents with equal foods merged, keeping order, or null when nothing merged. */ + static List merge(List items) { + List out = new ArrayList<>(); + boolean changed = false; + + for (ItemStack item : items) { + FoodItem food = FoodItem.fromItem(item); + int amount = item.getAmount(); + + for (int i = 0; i < out.size() && food != null && amount > 0; i++) { + ItemStack target = out.get(i); + int room = target.getMaxStackSize() - target.getAmount(); + if (room <= 0 || !sameFood(food, item, target)) continue; + + int add = Math.min(room, amount); + ItemStack kept = moreAged(target, item).clone(); + kept.setAmount(target.getAmount() + add); + out.set(i, kept); + amount -= add; + changed = true; + } + + if (amount > 0) { + ItemStack rest = item.clone(); + rest.setAmount(amount); + out.add(rest); + } + } + return changed ? out : null; + } + + private static boolean sameFood(FoodItem food, ItemStack item, ItemStack other) { + if (item.getType() != other.getType()) return false; + FoodItem otherFood = FoodItem.fromItem(other); + return otherFood != null + && InventoryAdder.equalsFood(food, otherFood) + && ConversionManager.sameApartFromAging(item, other); + } + + /** + * Equal foods age at the same rate, so whichever is further along now is the older one. + * Freshness decides, so food never gets fresher; the other aging tracks, such as warmth, only + * break a tie. + */ + static ItemStack moreAged(ItemStack a, ItemStack b) { + double[] ageA = age(a); + double[] ageB = age(b); + int order = Double.compare(ageA[0], ageB[0]); + if (order == 0) order = Double.compare(ageA[1], ageB[1]); + return order >= 0 ? a : b; + } + + /** How far the food has aged by now: freshness first, then every other aging track. */ + private static double[] age(ItemStack item) { + FoodItem food = FoodItem.fromItem(item); + food.updateAge(); + double[] age = new double[2]; + for (TagTrack track : food.getTagTracks()) { + if (!track.isAgeable()) continue; + int index = FRESHNESS.equals(track.getId()) ? 0 : 1; + age[index] += track.getValue() + food.getAgeRemainder(track.getId()); + } + return age; + } + + private static boolean isBundle(ItemStack item) { + return item != null && Tag.ITEMS_BUNDLES.isTagged(item.getType()); + } +} diff --git a/src/main/java/net/tfminecraft/cooking/manager/ConversionManager.java b/src/main/java/net/tfminecraft/cooking/manager/ConversionManager.java index 41bbd66..d5b8029 100644 --- a/src/main/java/net/tfminecraft/cooking/manager/ConversionManager.java +++ b/src/main/java/net/tfminecraft/cooking/manager/ConversionManager.java @@ -2,6 +2,8 @@ import net.tfminecraft.cooking.util.LegacyModelData; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import org.bukkit.Bukkit; @@ -172,28 +174,33 @@ public void onOpen(InventoryOpenEvent e) { int topSize = top.getSize(); int total = topSize + bottom.getSize(); - for (int i = total - 1; i >= 0; i--) { - ItemStack a = getSlot(i, top, bottom, topSize); - if (a == null) continue; - - FoodItem fa = FoodItem.fromItem(a); - if (fa == null) continue; - - for (int j = 0; j < total; j++) { - if (i == j) continue; - - ItemStack b = getSlot(j, top, bottom, topSize); - if (b == null) continue; + ItemStack[] items = new ItemStack[total]; + FoodItem[] foods = new FoodItem[total]; + for (int i = 0; i < total; i++) { + items[i] = getSlot(i, top, bottom, topSize); + foods[i] = items[i] == null ? null : FoodItem.fromItem(items[i]); + } - FoodItem fb = FoodItem.fromItem(b); - if (fb == null) continue; + // Equal foods all take the data of the most aged one, so opening a chest never makes food + // fresher. Ties go to the first slot. + for (int i = 0; i < total; i++) { + if (foods[i] == null) continue; + + List group = new ArrayList<>(List.of(i)); + ItemStack oldest = items[i]; + for (int j = i + 1; j < total; j++) { + if (foods[j] == null) continue; + if (!InventoryAdder.equalsFood(foods[i], foods[j]) || !sameApartFromAging(items[i], items[j])) continue; + group.add(j); + foods[j] = null; + oldest = BundleFoodStacker.moreAged(oldest, items[j]); + } - if (InventoryAdder.equalsFood(fa, fb) && sameApartFromAging(a, b)) { - ItemStack clone = b.clone(); - clone.setAmount(a.getAmount()); - setSlot(i, clone, top, bottom, topSize); - break; - } + for (int k : group) { + if (oldest.isSimilar(items[k])) continue; + ItemStack clone = oldest.clone(); + clone.setAmount(items[k].getAmount()); + setSlot(k, clone, top, bottom, topSize); } } } diff --git a/src/test/java/net/tfminecraft/cooking/liquid/LiquidContainerCoverageTest.java b/src/test/java/net/tfminecraft/cooking/liquid/LiquidContainerCoverageTest.java index 551ceaf..06cace7 100644 --- a/src/test/java/net/tfminecraft/cooking/liquid/LiquidContainerCoverageTest.java +++ b/src/test/java/net/tfminecraft/cooking/liquid/LiquidContainerCoverageTest.java @@ -113,15 +113,16 @@ void milkAgingUsesElapsedWholeSecondsAndCarriesScaledFractions() { fill("water", 2); LiquidContainerState.tickAge(barrel.furniture); assertFalse(barrel.variables.containsKey(LiquidContainerState.VAR_LAST_UPDATE)); fill("milk", 0); LiquidContainerState.tickAge(barrel.furniture); assertFalse(barrel.variables.containsKey(LiquidContainerState.VAR_LAST_UPDATE)); fill("milk", 2); LiquidContainerState.setMilkSnapshot(barrel.furniture, 3, 7, "Cow"); - barrel.variables.put(LiquidContainerState.VAR_LAST_UPDATE, System.currentTimeMillis() + 100_000L); - LiquidContainerState.tickAge(barrel.furniture); assertEquals(7, LiquidContainerState.getDairyFreshness(barrel.furniture)); - barrel.variables.put(LiquidContainerState.VAR_LAST_UPDATE, System.currentTimeMillis()); - LiquidContainerState.tickAge(barrel.furniture); assertEquals(7, LiquidContainerState.getDairyFreshness(barrel.furniture)); - barrel.variables.put(LiquidContainerState.VAR_LAST_UPDATE, System.currentTimeMillis() - 2_000L); - LiquidContainerState.tickAge(barrel.furniture); assertEquals(7, LiquidContainerState.getDairyFreshness(barrel.furniture)); + long now = 1_000_000L; + barrel.variables.put(LiquidContainerState.VAR_LAST_UPDATE, now + 100_000L); + LiquidContainerState.tickAge(barrel.furniture, now); assertEquals(7, LiquidContainerState.getDairyFreshness(barrel.furniture)); + barrel.variables.put(LiquidContainerState.VAR_LAST_UPDATE, now - 500L); + LiquidContainerState.tickAge(barrel.furniture, now); assertEquals(7, LiquidContainerState.getDairyFreshness(barrel.furniture)); + barrel.variables.put(LiquidContainerState.VAR_LAST_UPDATE, now - 2_000L); + LiquidContainerState.tickAge(barrel.furniture, now); assertEquals(7, LiquidContainerState.getDairyFreshness(barrel.furniture)); assertEquals(0.5, (Double) barrel.variables.get(LiquidContainerState.VAR_FRESHNESS_REMAINDER), 0.001); - barrel.variables.put(LiquidContainerState.VAR_LAST_UPDATE, System.currentTimeMillis() - 2_000L); - LiquidContainerState.tickAge(barrel.furniture); assertEquals(8, LiquidContainerState.getDairyFreshness(barrel.furniture)); + barrel.variables.put(LiquidContainerState.VAR_LAST_UPDATE, now - 2_000L); + LiquidContainerState.tickAge(barrel.furniture, now); assertEquals(8, LiquidContainerState.getDairyFreshness(barrel.furniture)); assertEquals(0.0, (Double) barrel.variables.get(LiquidContainerState.VAR_FRESHNESS_REMAINDER), 0.001); } diff --git a/src/test/java/net/tfminecraft/cooking/manager/BundleFoodStackerTest.java b/src/test/java/net/tfminecraft/cooking/manager/BundleFoodStackerTest.java new file mode 100644 index 0000000..a824ce0 --- /dev/null +++ b/src/test/java/net/tfminecraft/cooking/manager/BundleFoodStackerTest.java @@ -0,0 +1,245 @@ +package net.tfminecraft.cooking.manager; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.bukkit.Material; +import org.bukkit.configuration.MemoryConfiguration; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryType.SlotType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BundleMeta; +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; +import org.mockbukkit.mockbukkit.entity.PlayerMock; + +import net.tfminecraft.cooking.Cooking; +import net.tfminecraft.cooking.item.FoodItem; +import net.tfminecraft.cooking.item.tag.TagStep; +import net.tfminecraft.cooking.item.tag.TagTrack; +import net.tfminecraft.cooking.loader.FoodLoader; +import net.tfminecraft.cooking.loader.TrackLoader; +import net.tfminecraft.cooking.utils.ItemBuilder; + +/** Equal foods put into a bundle stack there as they do in an inventory, keeping the older data. */ +class BundleFoodStackerTest { + private Cooking previous; + private List foods; + private List tracks; + private ServerMock server; + private PlayerMock player; + private long now; + + @BeforeEach + void setUp() { + server = MockBukkit.mock(); + player = server.addPlayer(); + previous = Cooking.plugin; + Cooking.plugin = mock(Cooking.class); + when(Cooking.plugin.getName()).thenReturn("Cooking"); + when(Cooking.plugin.namespace()).thenReturn("cooking"); + foods = new ArrayList<>(FoodLoader.oList); + FoodLoader.oList.clear(); + tracks = new ArrayList<>(TrackLoader.oList); + TrackLoader.oList.clear(); + TrackLoader.oList.add(new TagTrack("freshness", true, List.of( + new TagStep("fresh", "Fresh", 0, 1, 1), new TagStep("stale", "Stale", 1_000_000, 1, 1)))); + TrackLoader.oList.add(new TagTrack("warmth", true, List.of(new TagStep("hot", "Hot", 0, 1, 1)))); + food("beef"); + food("pork"); + FoodLoader.getByString("pork").addOrModifyTrack(TrackLoader.getByString("warmth")); + now = System.currentTimeMillis(); + } + + @AfterEach + void tearDown() { + FoodLoader.oList.clear(); + FoodLoader.oList.addAll(foods); + TrackLoader.oList.clear(); + TrackLoader.oList.addAll(tracks); + MockBukkit.unmock(); + Cooking.plugin = previous; + } + + private static void food(String id) { + MemoryConfiguration config = new MemoryConfiguration(); + config.set("name", id); + FoodItem food = new FoodItem(id, config); + food.addOrModifyTrack(TrackLoader.getByString("freshness")); + FoodLoader.oList.add(food); + } + + /** A food whose stored freshness is value as of secondsAgo seconds ago. */ + private ItemStack meat(String id, Material type, int amount, int value, long secondsAgo) { + FoodItem food = new FoodItem(FoodLoader.getByString(id)); + food.getTagTrack("freshness").setValue(value); + food.setLastUpdate(now - secondsAgo * 1000L); + return ItemBuilder.stamp(new ItemStack(type, amount), food, id); + } + + /** Hot pork with the given freshness and warmth, stored now. */ + private ItemStack hotPork(int freshness, int warmth) { + FoodItem food = new FoodItem(FoodLoader.getByString("pork")); + food.getTagTrack("freshness").setValue(freshness); + food.getTagTrack("warmth").forceSetValue(warmth); + food.setLastUpdate(now); + return ItemBuilder.stamp(new ItemStack(Material.PORKCHOP), food, "pork"); + } + + private ItemStack beef(int amount, int value, long secondsAgo) { + return meat("beef", Material.BEEF, amount, value, secondsAgo); + } + + private static int freshness(ItemStack item) { + return FoodItem.fromItem(item).getTagTrack("freshness").getValue(); + } + + private static ItemStack bundle(ItemStack... items) { + ItemStack bundle = new ItemStack(Material.BUNDLE); + BundleMeta meta = (BundleMeta) bundle.getItemMeta(); + meta.setItems(List.of(items)); + bundle.setItemMeta(meta); + return bundle; + } + + private static List contents(ItemStack bundle) { + return ((BundleMeta) bundle.getItemMeta()).getItems(); + } + + @Test + void meatThatOnlyAgedDifferentlyStacksAndKeepsTheOlderData() { + // Stored freshness 50 ten seconds ago is younger than 5 stored a hundred seconds ago. + ItemStack younger = beef(2, 50, 10); + ItemStack older = beef(3, 5, 100); + + List merged = BundleFoodStacker.merge(List.of(younger, older)); + + assertEquals(1, merged.size()); + assertEquals(5, merged.get(0).getAmount()); + assertEquals(5, freshness(merged.get(0))); + } + + @Test + void otherItemsAndOtherFoodsStayApartInTheirOrder() { + ItemStack stone = new ItemStack(Material.STONE, 4); + ItemStack pork = meat("pork", Material.PORKCHOP, 1, 0, 0); + ItemStack stale = beef(1, 1_000_000, 0); + + assertNull(BundleFoodStacker.merge(List.of(stone, beef(1, 0, 0), pork, stale))); + + List merged = BundleFoodStacker.merge(List.of(stone, beef(1, 0, 0), pork, beef(2, 0, 30))); + assertEquals(List.of(Material.STONE, Material.BEEF, Material.PORKCHOP), + merged.stream().map(ItemStack::getType).toList()); + assertEquals(3, merged.get(1).getAmount()); + } + + @Test + void mergedStacksStopAtTheirMaximumAndTheRestStaysSeparate() { + List merged = BundleFoodStacker.merge(List.of(beef(60, 0, 0), beef(10, 0, 50))); + + assertEquals(2, merged.size()); + assertEquals(64, merged.get(0).getAmount()); + assertEquals(6, merged.get(1).getAmount()); + assertEquals(0, freshness(merged.get(1))); + + assertNull(BundleFoodStacker.merge(List.of(beef(64, 0, 0), beef(1, 0, 50)))); + } + + @Test + void equallyAgedFoodKeepsTheFirstCopy() { + ItemStack a = beef(1, 7, 0); + ItemStack b = beef(1, 7, 0); + + assertSame(a, BundleFoodStacker.moreAged(a, b)); + assertSame(b, BundleFoodStacker.moreAged(beef(1, 1, 0), b)); + } + + @Test + void freshnessDecidesWhichIsOlderAndWarmthOnlyBreaksATie() { + ItemStack staleButHot = hotPork(15, 10); + ItemStack freshButCool = hotPork(10, 19); + assertSame(staleButHot, BundleFoodStacker.moreAged(freshButCool, staleButHot)); + assertSame(staleButHot, BundleFoodStacker.moreAged(staleButHot, freshButCool)); + + ItemStack hot = hotPork(10, 5); + ItemStack cooler = hotPork(10, 9); + assertSame(cooler, BundleFoodStacker.moreAged(hot, cooler)); + } + + @Test + void onlyBundlesWithSomethingToMergeAreRewritten() { + assertNull(BundleFoodStacker.stack(null)); + assertNull(BundleFoodStacker.stack(beef(1, 0, 0))); + assertNull(BundleFoodStacker.stack(bundle(beef(1, 0, 0)))); + + ItemStack stacked = BundleFoodStacker.stack(bundle(beef(1, 0, 0), beef(1, 0, 20))); + assertEquals(1, contents(stacked).size()); + assertEquals(2, contents(stacked).get(0).getAmount()); + } + + @Test + void theCursorAndTheClickedSlotAreReadAgainAndRewritten() { + Inventory chest = server.createInventory(null, 27); + player.openInventory(chest); + player.setItemOnCursor(bundle(beef(1, 0, 0), beef(1, 0, 20))); + chest.setItem(4, bundle(beef(2, 0, 0), beef(2, 0, 20))); + + BundleFoodStacker.stackBundles(player, chest, 4); + + assertEquals(2, contents(player.getItemOnCursor()).get(0).getAmount()); + assertEquals(4, contents(chest.getItem(4)).get(0).getAmount()); + } + + @Test + void anInventoryNoLongerOpenOrNoSlotAtAllIsLeftAlone() { + Inventory chest = server.createInventory(null, 27); + chest.setItem(4, bundle(beef(1, 0, 0), beef(1, 0, 20))); + + BundleFoodStacker.stackBundles(player, chest, 4); + BundleFoodStacker.stackBundles(player, null, -999); + + assertEquals(2, contents(chest.getItem(4)).size()); + + player.getInventory().setItem(2, new ItemStack(Material.STONE)); + BundleFoodStacker.stackBundles(player, player.getInventory(), 2); + assertEquals(Material.STONE, player.getInventory().getItem(2).getType()); + } + + @Test + void onlyClicksInvolvingABundleScheduleAMerge() { + List tasks = new ArrayList<>(); + BundleFoodStacker stacker = new BundleFoodStacker(tasks::add); + Inventory chest = server.createInventory(null, 27); + var view = player.openInventory(chest); + + chest.setItem(0, beef(1, 0, 0)); + stacker.onClick(new InventoryClickEvent(view, SlotType.CONTAINER, 0, ClickType.LEFT, InventoryAction.PICKUP_ALL)); + assertEquals(0, tasks.size()); + + chest.setItem(0, bundle(beef(1, 0, 0), beef(1, 0, 20))); + stacker.onClick(new InventoryClickEvent(view, SlotType.CONTAINER, 0, ClickType.RIGHT, InventoryAction.PICKUP_HALF)); + chest.setItem(0, null); + player.setItemOnCursor(bundle(beef(1, 0, 0), beef(1, 0, 20))); + stacker.onClick(new InventoryClickEvent(view, SlotType.CONTAINER, 0, ClickType.RIGHT, InventoryAction.PICKUP_HALF)); + assertEquals(2, tasks.size()); + + chest.setItem(0, bundle(beef(1, 0, 0), beef(1, 0, 20))); + tasks.forEach(Runnable::run); + assertEquals(1, contents(chest.getItem(0)).size()); + assertEquals(1, contents(player.getItemOnCursor()).size()); + + new BundleFoodStacker(); + } +} diff --git a/src/test/java/net/tfminecraft/cooking/manager/ChestOpenAgingTest.java b/src/test/java/net/tfminecraft/cooking/manager/ChestOpenAgingTest.java new file mode 100644 index 0000000..a7da53c --- /dev/null +++ b/src/test/java/net/tfminecraft/cooking/manager/ChestOpenAgingTest.java @@ -0,0 +1,136 @@ +package net.tfminecraft.cooking.manager; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.bukkit.Material; +import org.bukkit.configuration.MemoryConfiguration; +import org.bukkit.event.inventory.InventoryOpenEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; +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; +import org.mockbukkit.mockbukkit.entity.PlayerMock; + +import net.tfminecraft.cooking.Cooking; +import net.tfminecraft.cooking.item.FoodItem; +import net.tfminecraft.cooking.item.tag.TagStep; +import net.tfminecraft.cooking.item.tag.TagTrack; +import net.tfminecraft.cooking.loader.FoodLoader; +import net.tfminecraft.cooking.loader.TrackLoader; +import net.tfminecraft.cooking.utils.ItemBuilder; +import net.tfminecraft.cooking.utils.Keys; + +/** Opening a chest makes equal foods stack by giving them all the most aged copy's data. */ +class ChestOpenAgingTest { + private Cooking previous; + private List foods; + private List tracks; + private ServerMock server; + private PlayerMock player; + private Inventory chest; + private long now; + + @BeforeEach + void setUp() { + server = MockBukkit.mock(); + player = server.addPlayer(); + previous = Cooking.plugin; + Cooking.plugin = mock(Cooking.class); + when(Cooking.plugin.getName()).thenReturn("Cooking"); + when(Cooking.plugin.namespace()).thenReturn("cooking"); + foods = new ArrayList<>(FoodLoader.oList); + FoodLoader.oList.clear(); + tracks = new ArrayList<>(TrackLoader.oList); + TrackLoader.oList.clear(); + TrackLoader.oList.add(new TagTrack("freshness", true, List.of( + new TagStep("fresh", "Fresh", 0, 1, 1), new TagStep("stale", "Stale", 1_000_000, 1, 1)))); + MemoryConfiguration config = new MemoryConfiguration(); + config.set("name", "beef"); + FoodItem beef = new FoodItem("beef", config); + beef.addOrModifyTrack(TrackLoader.getByString("freshness")); + FoodLoader.oList.add(beef); + chest = server.createInventory(null, 9); + now = System.currentTimeMillis(); + } + + @AfterEach + void tearDown() { + FoodLoader.oList.clear(); + FoodLoader.oList.addAll(foods); + TrackLoader.oList.clear(); + TrackLoader.oList.addAll(tracks); + MockBukkit.unmock(); + Cooking.plugin = previous; + } + + /** Beef whose stored freshness is value as of secondsAgo seconds ago. */ + private ItemStack beef(int amount, int value, long secondsAgo) { + FoodItem food = new FoodItem(FoodLoader.getByString("beef")); + food.getTagTrack("freshness").setValue(value); + food.setLastUpdate(now - secondsAgo * 1000L); + return ItemBuilder.stamp(new ItemStack(Material.BEEF, amount), food, "beef"); + } + + private void open() { + InventoryView view = mock(InventoryView.class); + when(view.getTopInventory()).thenReturn(chest); + when(view.getPlayer()).thenReturn(player); + new ConversionManager().onOpen(new InventoryOpenEvent(view)); + } + + private static long clock(ItemStack item) { + return item.getItemMeta().getPersistentDataContainer().get(Keys.LAST_UPDATE, PersistentDataType.LONG); + } + + @Test + void theFresherFoodTakesTheOlderDataWhereverEitherIs() { + chest.setItem(0, beef(2, 0, 0)); + chest.setItem(5, beef(1, 0, 100)); + player.getInventory().setItem(3, beef(4, 0, 0)); + + open(); + + long older = now - 100_000L; + assertEquals(older, clock(chest.getItem(0))); + assertEquals(older, clock(chest.getItem(5))); + assertEquals(older, clock(player.getInventory().getItem(3))); + assertEquals(2, chest.getItem(0).getAmount()); + assertEquals(1, chest.getItem(5).getAmount()); + assertEquals(4, player.getInventory().getItem(3).getAmount()); + } + + @Test + void equallyAgedFoodAllTakesTheFirstSlotsCopy() { + // Ten points stored now and none stored ten seconds ago are the same age. + chest.setItem(1, beef(1, 10, 0)); + chest.setItem(4, beef(1, 0, 10)); + + open(); + + assertEquals(now, clock(chest.getItem(1))); + assertEquals(now, clock(chest.getItem(4))); + } + + @Test + void foodAtAnotherFreshnessStepKeepsItsOwnData() { + chest.setItem(0, beef(1, 0, 0)); + chest.setItem(1, beef(1, 1_000_000, 0)); + chest.setItem(2, new ItemStack(Material.STONE)); + + open(); + + assertEquals(1_000_000, FoodItem.fromItem(chest.getItem(1)).getTagTrack("freshness").getValue()); + assertEquals(0, FoodItem.fromItem(chest.getItem(0)).getTagTrack("freshness").getValue()); + assertEquals(Material.STONE, chest.getItem(2).getType()); + } +}