Skip to content
Draft
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
Expand Up @@ -12,7 +12,7 @@
import net.tfminecraft.cooking.Cooking;
import net.tfminecraft.cooking.heat.HeatSources;
import net.tfminecraft.cooking.item.FoodItem;
import net.tfminecraft.cooking.utils.ItemUpdater;
import net.tfminecraft.cooking.utils.SlotAging;
import net.tfminecraft.interactiblefurniture.furniture.Furniture;
import net.tfminecraft.interactiblefurniture.furniture.PlacedFurnitureSlot;
import net.tfminecraft.interactiblefurniture.furniture.PlacedSlot;
Expand Down Expand Up @@ -81,7 +81,7 @@ private void ageTray(Furniture tray, BakingTrayRecipe recipe) {

private boolean ageSlot(PlacedSlot slot, Furniture tray) {
ItemStack item = slot.getCurrentItem();
if (item == null || item.getType().isAir()) {
if (item == null || item.getType().isAir() || SlotAging.isResting(slot, item)) {
return false;
}

Expand All @@ -90,7 +90,7 @@ private boolean ageSlot(PlacedSlot slot, Furniture tray) {
return false;
}

ItemStack updated = ItemUpdater.updateItem(item, foodItem, tray.getId());
ItemStack updated = SlotAging.age(slot, item, foodItem, tray.getId());
if (updated == null || updated.getType().isAir()) {
return false;
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/net/tfminecraft/cooking/carve/CarveHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ public static boolean tryCarve(Player player, Furniture furniture, PlacedSlot sl

if (roast == null) return false;

// The hook only rewrites a roast when it looks different, so bring its aging up to now before the cuts and
// the roast left on the hook (which starts a new clock) take its numbers.
roast.updateAge();

CarvableRoastUtils.readCarveState(roast, stack);

if (!CarvableRoastUtils.isCarvable(roast)) return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import net.tfminecraft.cooking.carve.CarvableRoastUtils;
import net.tfminecraft.cooking.carve.CarveHandler;
import net.tfminecraft.cooking.item.FoodItem;
import net.tfminecraft.cooking.utils.ItemUpdater;
import net.tfminecraft.cooking.utils.SlotAging;
import net.tfminecraft.cooking.utils.StationChunks;
import net.tfminecraft.interactiblefurniture.events.FurniturePunchEvent;
import net.tfminecraft.interactiblefurniture.events.FurnitureSlotItemAddEvent;
Expand Down Expand Up @@ -107,14 +107,15 @@ private static void tickHook(Furniture furniture) {
return;
}
ItemStack stack = slot.getCurrentItem();
if (stack == null) {
// An unchanged roast that can't look different yet keeps its item and display as they are.
if (stack == null || SlotAging.isResting(slot, stack)) {
return;
}
FoodItem foodItem = FoodItem.fromItem(stack);
if (foodItem == null) {
return;
}
ItemStack updated = ItemUpdater.updateItem(stack, foodItem, furniture.getId());
ItemStack updated = SlotAging.age(slot, stack, foodItem, furniture.getId());
if (updated == null || updated.getType().isAir()) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ private void tickTray(Furniture consumer, Furniture tray, BakingTrayRecipe recip
}

BakingTrayState.incrementSlotElapsed(tray, slotId);
// Save the bake time with the next autosave: the tray's aging timer no longer rewrites it every second.
changed = true;
int elapsed = BakingTrayState.getSlotElapsed(tray, slotId);
int cookedValue = cooked == null ? RAW : cooked.getValue();

Expand Down
7 changes: 4 additions & 3 deletions src/main/java/net/tfminecraft/cooking/trough/TroughAging.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import net.tfminecraft.cooking.Cooking;
import net.tfminecraft.cooking.cache.FurnitureCache;
import net.tfminecraft.cooking.item.FoodItem;
import net.tfminecraft.cooking.utils.ItemUpdater;
import net.tfminecraft.cooking.utils.SlotAging;
import net.tfminecraft.interactiblefurniture.furniture.Furniture;
import net.tfminecraft.interactiblefurniture.furniture.PlacedSlot;

Expand Down Expand Up @@ -59,7 +59,8 @@ private void ageTrough(Furniture furniture) {

private boolean ageSlot(Furniture furniture, PlacedSlot slot, String slotId) {
ItemStack item = slot.getCurrentItem();
if (item == null || item.getType().isAir()) {
// A resting food was neither rotten nor unknown when last aged, and it can't have gone off since.
if (item == null || item.getType().isAir() || SlotAging.isResting(slot, item)) {
return false;
}

Expand All @@ -69,7 +70,7 @@ private boolean ageSlot(Furniture furniture, PlacedSlot slot, String slotId) {
return true;
}

ItemStack updated = ItemUpdater.updateItem(item, foodItem, furniture.getId());
ItemStack updated = SlotAging.age(slot, item, foodItem, furniture.getId());
if (updated != null && updated.getType().isAir()) {
updated = null;
}
Expand Down
21 changes: 15 additions & 6 deletions src/main/java/net/tfminecraft/cooking/utils/FoodScan.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
* remainder, so writing the aged numbers back changes nothing a player sees and the food ages the same either way.
* The scan therefore only rewrites a food that would look different (a new tag step, warmth gone, lore or model built
* differently, as after a reload or an update), whose clock must be set, or that has a sauce (below). It also says
* when the unchanged food could next look different, so TLibs can skip the slot until then.
* when the unchanged food could next look different, so TLibs can skip the slot until then. SlotAging does the same
* for food lying in furniture.
*/
public final class FoodScan {

Expand All @@ -37,18 +38,26 @@ public record Result(ItemStack item, long recheckAt) {}
* Ages the food (parsed from stack, whose meta is meta) to now. A held food is only rewritten when today's held
* rule says so (a new step, warmth gone or broken lore), so eating it isn't interrupted.
*/
public static Result update(ItemStack stack, ItemMeta meta, FoodItem fi, boolean held) {
return update(stack, meta, fi, held, null);
}

/**
* As {@link #update(ItemStack, ItemMeta, FoodItem, boolean)} for food in a slot of the furniture whose id is
* furniture (null in an inventory), which picks the model the food is built with there.
*/
// Keep the existing legacy text representation, formatting, and exact-string comparisons.
@SuppressWarnings("deprecation")
public static Result update(ItemStack stack, ItemMeta meta, FoodItem fi, boolean held) {
public static Result update(ItemStack stack, ItemMeta meta, FoodItem fi, boolean held, String furniture) {
PersistentDataContainer pdc = meta.getPersistentDataContainer();
boolean broken = ItemUpdater.needsLoreRebuild(meta.getLore(), pdc.has(Keys.LORE_INDEX_MAP, PersistentDataType.STRING));
if (!fi.shouldUpdate()) {
return new Result(broken ? ItemUpdater.applyItemUpdate(stack, fi, null) : null, ItemScanHandler.WHEN_CHANGED);
return new Result(broken ? ItemUpdater.applyItemUpdate(stack, fi, furniture) : null, ItemScanHandler.WHEN_CHANGED);
}
if (fi.hasSauce()) {
// A sauce keeps no clock or remainder of its own, so it only ages by whole steps each time the food is
// rewritten. Rewrite it on every scan as before, so sauces age exactly as they always have.
return new Result(ItemUpdater.updateItem(stack, fi, null, held), ItemScanHandler.EVERY_SCAN);
return new Result(ItemUpdater.updateItem(stack, fi, furniture, held), ItemScanHandler.EVERY_SCAN);
}
long now = StackNormalizer.quantizedNow();
Long stored = pdc.get(Keys.LAST_UPDATE, PersistentDataType.LONG);
Expand All @@ -58,12 +67,12 @@ public static Result update(ItemStack stack, ItemMeta meta, FoodItem fi, boolean
fi.setLastUpdate(now);
long recheckAt = held ? ItemScanHandler.EVERY_SCAN : nextChange(fi, now);
if (changed || expired || broken || (!held && clockNeedsWriting(stored, now))) {
return new Result(ItemUpdater.applyItemUpdate(stack, fi, null), recheckAt);
return new Result(ItemUpdater.applyItemUpdate(stack, fi, furniture), recheckAt);
}
if (held) {
return new Result(null, recheckAt);
}
ItemStack rebuilt = ItemUpdater.applyItemUpdate(stack.clone(), fi, null);
ItemStack rebuilt = ItemUpdater.applyItemUpdate(stack.clone(), fi, furniture);
return new Result(looksTheSame(stack, meta, rebuilt) ? null : rebuilt, recheckAt);
}

Expand Down
6 changes: 5 additions & 1 deletion src/main/java/net/tfminecraft/cooking/utils/ItemRef.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,13 @@ public static ItemStack apply(String ref, ItemStack source) {
return out;
}

/** Drops every kept template; the next use of each ref builds it again. */
/**
* Drops every kept template; the next use of each ref builds it again. Runs on /cooking reload and /iareload, so
* food resting in furniture slots is looked at again too: it may now be built differently.
*/
public static void clearCache() {
templates = null;
SlotAging.forgetAll();
}

/** Turns template keeping on or off; either way the kept templates are dropped. */
Expand Down
73 changes: 73 additions & 0 deletions src/main/java/net/tfminecraft/cooking/utils/SlotAging.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package net.tfminecraft.cooking.utils;

import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;

import net.tfminecraft.cooking.item.FoodItem;
import net.tfminecraft.interactiblefurniture.furniture.PlacedSlot;
import net.tfminecraft.tlibs.itemscan.ItemScanHandler;

/**
* Ages the food lying in a furniture slot (a meat hook, a baking tray, a trough) on its station's one-second timer,
* the way the inventory scan ages unheld food (FoodScan). The timers used to rebuild and rewrite every such food each
* second only to store its new clock and aged numbers; the food ages the same from its stored clock, values and
* remainder, so the slot is now only written when the food would look different.
*
* <p>Like TLibs' scan, it keeps a copy of each slot's food that it left as it was, and when that food can next look
* different. While the slot holds an equal item and that time hasn't come, the timer skips the slot. Any change to the
* item (taken, replaced, carved, glazed, changed in place) ends the skip, as does {@link #forgetAll()}.
*/
public final class SlotAging {

/** A slot's food as the timer last left it, when it can next look different, and the generation it rests in. */
private record Resting(ItemStack item, long until, int generation) {}

// By slot identity; a slot dropped by its furniture drops out on its own. Only the server thread touches it.
private static final Map<PlacedSlot, Resting> RESTING = new WeakHashMap<>();

// forgetAll can come from an ItemsAdder event on any thread, so it only moves this on and rests from before end.
private static final AtomicInteger GENERATION = new AtomicInteger();

private SlotAging() {}

/** True while the slot still holds the item the timer last left there and that item can't look different yet. */
public static boolean isResting(PlacedSlot slot, ItemStack stack) {
Resting resting = RESTING.get(slot);
if (resting == null) {
return false;
}
if (resting.generation() == GENERATION.get() && resting.until() > StackNormalizer.quantizedNow()
&& resting.item().equals(stack)) {
return true;
}
RESTING.remove(slot);
return false;
}

/**
* Ages the food (parsed from stack) in a slot of the furniture whose id is furniture, and returns what to put in
* the slot, or null to leave it. A food left as it was rests until it can look different; a written one is looked
* at again on the next pass, as the scan does.
*/
public static ItemStack age(PlacedSlot slot, ItemStack stack, FoodItem food, String furniture) {
// Read first: a rest must not outlive a forgetAll that came while this food was being built.
int generation = GENERATION.get();
ItemMeta meta = stack.getItemMeta();
FoodScan.Result result = FoodScan.update(stack, meta, food, false, furniture);
if (result.item() == null && result.recheckAt() != ItemScanHandler.EVERY_SCAN) {
RESTING.put(slot, new Resting(stack.clone(), result.recheckAt(), generation));
} else {
RESTING.remove(slot);
}
return result.item();
}

/** Ends every skip, so each slot's food is looked at again: its config or item templates may have changed. */
public static void forgetAll() {
GENERATION.incrementAndGet();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import net.tfminecraft.cooking.utils.InventoryAdder;
import net.tfminecraft.cooking.utils.ItemBuilder;
import net.tfminecraft.cooking.utils.ItemUpdater;
import net.tfminecraft.cooking.utils.SlotAging;
import net.tfminecraft.cooking.utils.WarmthUtils;
import net.tfminecraft.interactiblefurniture.events.FurnitureInteractEvent;
import net.tfminecraft.interactiblefurniture.furniture.Furniture;
Expand Down Expand Up @@ -413,6 +414,15 @@ void agingVisitsLooseAndNestedTraysOnceAndIgnoresUnsupportedContents() {
verify(nested, times(1)).forceModel(any()); verify(separate, times(1)).forceModel(any());
}

@Test
void restingBreadIsLeftAloneUntilItCanLookDifferent() {
ItemStack original = bakery.loaf(0, 1); PlacedSlot slot = tray.place("left_a", original);
bakery.aging.when(() -> SlotAging.isResting(slot, original)).thenReturn(true);
new BakingTrayAging().start(); env.server.getScheduler().performTicks(20);
assertSame(original, slot.getCurrentItem()); verify(slot, never()).forceModel(any());
bakery.aging.verify(() -> SlotAging.age(any(), any(), any(), any()), never()); verify(env.manager, never()).markDirty(tray.furniture);
}

@Test
void emptyEntriesInThePublicSlotMapDoNotPreventOtherLoavesFromAging() {
tray.furniture.getActiveSlots().put("left_a", null);
Expand All @@ -430,6 +440,7 @@ public static final class Bakery implements AutoCloseable {
public final Set<Furniture> consumers = new HashSet<>();
public final Map<Furniture, Map<String, PlacedFurnitureSlot>> nested = new LinkedHashMap<>();
public final MockedStatic<ItemUpdater> updater;
public final MockedStatic<SlotAging> aging;
public final MockedStatic<HeatSources> heat;
public final Station tray;
public BakingTrayRecipe recipe;
Expand All @@ -445,6 +456,9 @@ public Bakery() {
updater = env.scoped(mockStatic(ItemUpdater.class));
updater.when(() -> ItemUpdater.applyItemUpdate(any(), any(), any())).thenAnswer(inv -> updated(inv.getArgument(0), inv.getArgument(1)));
updater.when(() -> ItemUpdater.updateItem(any(), any(), any())).thenAnswer(inv -> updated(inv.getArgument(0), inv.getArgument(1)));
// The timer ages through SlotAging; route it to the stubbed ItemUpdater (SlotAgingTest covers the real one).
aging = env.scoped(mockStatic(SlotAging.class));
aging.when(() -> SlotAging.age(any(), any(), any(), any())).thenAnswer(inv -> ItemUpdater.updateItem(inv.getArgument(1), inv.getArgument(2), inv.getArgument(3)));
heat = env.scoped(mockStatic(HeatSources.class));
heat.when(() -> HeatSources.isConsumer(any())).thenAnswer(inv -> consumers.contains(inv.getArgument(0)));
heat.when(() -> HeatSources.consumerHasHeat(any())).thenAnswer(inv -> heated);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import net.tfminecraft.cooking.utils.ItemBuilder;
import net.tfminecraft.cooking.utils.ItemRef;
import net.tfminecraft.cooking.utils.ItemUpdater;
import net.tfminecraft.cooking.utils.SlotAging;
import net.tfminecraft.cooking.utils.Keys;
import net.tfminecraft.interactiblefurniture.events.FurniturePunchEvent;
import net.tfminecraft.interactiblefurniture.furniture.Furniture;
Expand All @@ -71,6 +72,7 @@ class CarvingAndMeatHookCoverageTest {
MockedStatic<FoodParser> parser;
MockedStatic<ItemRef> refs;
MockedStatic<ItemUpdater> updater;
MockedStatic<SlotAging> aging;
String oldHook, oldTool;
FoodItem lastPart;

Expand All @@ -97,6 +99,9 @@ class CarvingAndMeatHookCoverageTest {
updater = env.scoped(mockStatic(ItemUpdater.class));
updater.when(() -> ItemUpdater.applyItemUpdate(any(), any(), any())).thenAnswer(inv -> updated(inv.getArgument(1)));
updater.when(() -> ItemUpdater.updateItem(any(), any(), any())).thenAnswer(inv -> updated(inv.getArgument(1)));
// The hook's timer ages through SlotAging; route it to the stubbed ItemUpdater (SlotAgingTest covers the real one).
aging = env.scoped(mockStatic(SlotAging.class));
aging.when(() -> SlotAging.age(any(), any(), any(), any())).thenAnswer(inv -> ItemUpdater.updateItem(inv.getArgument(1), inv.getArgument(2), inv.getArgument(3)));
load("""
roast:
start-remaining: 3
Expand Down Expand Up @@ -412,6 +417,24 @@ void meatHookTimersAgeOnlyReadableRoastsAndRefreshTheirStageDisplay() {
verify(slot).setCurrentItem(any()); verify(slot).applyDisplayData(any()); verify(env.manager).markDirty(hook.furniture);
}

@Test
void carvingAgesTheRoastToNowBeforeTheCutsTakeItsNumbers() {
// The hook leaves a resting roast's stored numbers alone, so they can be well behind the clock.
FoodItem roast = roast("roast"); track(roast, "freshness", 0); roast.setLastUpdate(System.currentTimeMillis() - 5_000L);
PlacedSlot slot = hook.place("content", whole(roast));
assertTrue(carve(slot));
assertTrue(lastPart.getTagTrack("freshness").getValue() >= 5, "the cut keeps the roast's five seconds of aging");
}

@Test
void aRestingRoastKeepsItsItemAndDisplayUntilItCanLookDifferent() {
PlacedSlot slot = placeRoast(); ItemStack original = slot.getCurrentItem();
aging.when(() -> SlotAging.isResting(slot, original)).thenReturn(true);
hooks.start(); env.server.getScheduler().performTicks(20);
assertSame(original, slot.getCurrentItem()); verify(slot, never()).setCurrentItem(any()); verify(slot, never()).applyDisplayData(any());
aging.verify(() -> SlotAging.age(any(), any(), any(), any()), never()); verify(env.manager, never()).markDirty(hook.furniture);
}

@Test
void airAgingResultsCannotReplaceAHangingRoast() {
PlacedSlot slot = placeRoast(); ItemStack original = slot.getCurrentItem();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,8 @@ void cavityClocksPauseWithoutHeatThenCookAndBurnEachLoafAtItsOwnThreshold() {
assertEquals(2, env.resolve(bakery.tray.active.get("left_b").getCurrentItem()).getTagTrack("cooked").getValue());
assertEquals(0, BakingTrayState.getSlotElapsed(bakery.tray.furniture, "right_a"));
assertEquals(0, BakingTrayState.getSlotElapsed(bakery.tray.furniture, "right_b"));
verify(env.manager, times(2)).markDirty(bakery.tray.furniture);
// Once per heated second: the bake times advance and are saved with them.
verify(env.manager, times(4)).markDirty(bakery.tray.furniture);
}

@Test
Expand Down Expand Up @@ -374,6 +375,8 @@ void failedBakingRenderDoesNotReplaceTheDisplayedLoafOrClaimASuccessfulChange()
bakery.updater.when(() -> ItemUpdater.applyItemUpdate(any(), any(), any())).thenReturn(null);
new OvenCavityManager().start(); env.server.getScheduler().performTicks(40);
assertSame(original, slot.getCurrentItem()); verify(slot, never()).forceModel(any());
verify(env.manager, never()).markDirty(bakery.tray.furniture);
// Only the bake time advanced (and is saved); the loaf itself stays as it was.
assertEquals(2, BakingTrayState.getSlotElapsed(bakery.tray.furniture, "left_a"));
verify(env.manager, times(2)).markDirty(bakery.tray.furniture);
}
}
Loading
Loading