diff --git a/src/main/java/net/tfminecraft/cooking/cooking/SauceReference.java b/src/main/java/net/tfminecraft/cooking/cooking/SauceReference.java index ab2a94a..5226494 100644 --- a/src/main/java/net/tfminecraft/cooking/cooking/SauceReference.java +++ b/src/main/java/net/tfminecraft/cooking/cooking/SauceReference.java @@ -64,6 +64,8 @@ import net.tfminecraft.cooking.utils.StationAddonRules; +import net.tfminecraft.interactiblefurniture.InteractibleFurniture; + import net.tfminecraft.interactiblefurniture.events.FurnitureInteractEvent; import net.tfminecraft.interactiblefurniture.furniture.Furniture; @@ -74,6 +76,9 @@ public class SauceReference extends CookingReference { + private static final String VAR_COLOURS = "sauce.colours"; + private static final String VAR_LIQUID = "sauce.liquid"; + private FoodItem liquidSource; public SauceReference(Furniture f, Method m) { @@ -382,6 +387,7 @@ public void interact(FurnitureInteractEvent e) { if (poured != null) { liquidSource = new FoodItem(poured); } + saveLiquid(poured != null ? item : null); PlacedSlot slot = f.getOrCreatePlacedSlot("liquid"); @@ -392,6 +398,7 @@ public void interact(FurnitureInteractEvent e) { slot.forceModel(TLibs.getItemAPI().getCreator().getItemFromPath(modelPath)); addColour(ItemCache.getColour(item)); + saveColours(); consumeSaucepanPour(p, item); @@ -413,6 +420,8 @@ public void interact(FurnitureInteractEvent e) { if(add(slot, item)) { + saveColours(); + updateModel(); p.swingMainHand(); @@ -443,6 +452,70 @@ public void updateModel() { public void clear() { liquidSource = null; super.clear(); + if (f == null) return; + f.getVariables().remove(VAR_COLOURS); + f.getVariables().remove(VAR_LIQUID); + InteractibleFurniture.getInstance().getFurnitureManager().markDirty(f); + } + + /** + * The base rebuild keeps raw food only. A chunk reload or restart dropped the liquid, so the + * saucepan still showed sauce that the ladle could no longer scoop. + */ + @Override + public void rebuildFromFurniture() { + super.rebuildFromFurniture(); + if (f == null || f.getType() == null) return; + restoreIngredients(); + if (!f.hasActiveSlot("liquid")) return; + secondaries.put("liquid", -1); + ItemStack liquid = PotReference.decodeStack(stringVariable(VAR_LIQUID)); + liquidSource = liquid == null ? null : FoodItem.fromItem(liquid); + restoreColours(); + } + + /** Seasonings, sweeteners and other additions are not raw, so the base rebuild skips them. */ + private void restoreIngredients() { + for (String slotId : f.getType().getSlots().keySet()) { + if (!slotId.contains("input") || slots.containsKey(slotId)) continue; + PlacedSlot placed = f.getActiveSlot(slotId).orElse(null); + if (placed == null) continue; + FoodItem fi = FoodItem.fromItem(placed.getCurrentItem()); + if (fi != null) slots.put(slotId, fi); + } + } + + /** Saucepans filled before colours were saved fall back to their ingredients' colours. */ + private void restoreColours() { + String saved = stringVariable(VAR_COLOURS); + if (saved != null && !saved.isBlank()) { + for (String hex : saved.split(",")) addColour(hex); + return; + } + for (String slotId : slots.keySet()) { + f.getActiveSlot(slotId).ifPresent(placed -> addColour(ItemCache.getColour(placed.getCurrentItem()))); + } + } + + private String stringVariable(String key) { + return f.getVariables().get(key) instanceof String text ? text : null; + } + + private void saveColours() { + f.getVariables().put(VAR_COLOURS, String.join(",", colours)); + InteractibleFurniture.getInstance().getFurnitureManager().markDirty(f); + } + + /** Keeps the poured liquid's food data for the scoop's quality after a reload. */ + private void saveLiquid(ItemStack poured) { + String payload = null; + if (poured != null) { + ItemStack single = poured.clone(); + single.setAmount(1); + payload = PotReference.encodeStack(single); + } + if (payload == null) f.getVariables().remove(VAR_LIQUID); + else f.getVariables().put(VAR_LIQUID, payload); } private static boolean canPourSaucepanLiquid(ItemStack item) { diff --git a/src/test/java/net/tfminecraft/cooking/cooking/SauceReferenceReloadTest.java b/src/test/java/net/tfminecraft/cooking/cooking/SauceReferenceReloadTest.java new file mode 100644 index 0000000..c35194e --- /dev/null +++ b/src/test/java/net/tfminecraft/cooking/cooking/SauceReferenceReloadTest.java @@ -0,0 +1,127 @@ +package net.tfminecraft.cooking.cooking; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import net.tfminecraft.cooking.cache.ItemCache; +import net.tfminecraft.cooking.enums.Method; +import net.tfminecraft.cooking.quality.CompositionQualityResolver; +import net.tfminecraft.cooking.utils.Keys; +import org.bukkit.Material; +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; + +/** A chunk reload or restart rebuilds the saucepan from its furniture; the sauce must still scoop. */ +class SauceReferenceReloadTest { + private CookingReferencesTest.Environment env; + + @BeforeEach void setUp() { + env = new CookingReferencesTest.Environment(); + env.cache.when(() -> ItemCache.getColour(any())).thenAnswer(inv -> + CookingReferencesTest.Environment.material(inv.getArgument(0), Material.WHITE_DYE) ? "ffffff" : "aa5500"); + } + @AfterEach void tearDown() { env.close(); } + + private SauceReference saucepanWithMilkAndSalt() { + SauceReference sauce = new SauceReference(env.furniture, Method.SAUCEPAN); + env.player.getInventory().setItemInMainHand(env.stack(env.food("milk", "milk", "Milk"), Material.WHITE_DYE, 1)); + sauce.interact(env.interaction()); + env.player.getInventory().setItemInMainHand(env.stack(env.food("salt", "salt", "Salt"), Material.SUGAR, 2)); + sauce.interact(env.interaction()); + return sauce; + } + + private SauceReference rebuilt() { + SauceReference restored = new SauceReference(env.furniture, Method.SAUCEPAN); + restored.rebuildFromFurniture(); + return restored; + } + + private ItemStack scoop(SauceReference sauce) { + ItemStack ladle = new ItemStack(Material.PAPER); + env.player.getInventory().setItemInMainHand(ladle); + sauce.interact(env.interaction()); + return env.player.getInventory().getItemInMainHand(); + } + + @Test + void aRebuiltSaucepanKeepsItsLiquidIngredientsAndColoursAndStillScoops() { + SauceReference sauce = saucepanWithMilkAndSalt(); + assertEquals("ffffff,aa5500", env.variables.get("sauce.colours")); + assertNotNull(env.variables.get("sauce.liquid")); + + SauceReference restored = rebuilt(); + + assertEquals(Map.of("liquid", -1), restored.getSecondaries()); + assertEquals(sauce.getSlots().keySet(), restored.getSlots().keySet()); + assertEquals("salt", restored.getSlots().values().iterator().next().getCategory()); + assertEquals(List.of("ffffff", "aa5500"), restored.getColours()); + + ItemStack served = scoop(restored); + assertTrue(served.getItemMeta().getPersistentDataContainer().has(Keys.SAUCE_COLOUR, PersistentDataType.STRING)); + assertNotNull(env.lastBuilt.getTagTrack("sauce_creamy"), "The saved milk keeps the sauce creamy"); + env.composer.verify(() -> CompositionQualityResolver.compose(any(), argThat(inputs -> inputs.size() == 2), any())); + assertTrue(restored.isEmpty()); + assertTrue(env.active.isEmpty()); + assertFalse(env.variables.containsKey("sauce.colours")); + assertFalse(env.variables.containsKey("sauce.liquid")); + } + + @Test + void saucepansFilledBeforeTheFixUseTheirIngredientsColours() { + saucepanWithMilkAndSalt(); + env.variables.clear(); + + SauceReference restored = rebuilt(); + + assertEquals(Map.of("liquid", -1), restored.getSecondaries()); + assertEquals(List.of("aa5500"), restored.getColours()); + ItemStack served = scoop(restored); + assertEquals(Material.PAPER, served.getType()); + env.composer.verify(() -> CompositionQualityResolver.compose(any(), argThat(inputs -> inputs.size() == 1), any())); + assertTrue(restored.isEmpty()); + } + + @Test + void ingredientsWithoutLiquidAreKeptButNothingCanBeScooped() { + env.place("input_1", env.stack(env.food("salt", "salt", "Salt"), Material.SUGAR, 1)); + env.place("input_2", new ItemStack(Material.STONE)); + + SauceReference restored = rebuilt(); + + assertEquals(Set.of("input_1"), restored.getSlots().keySet()); + assertTrue(restored.getSecondaries().isEmpty()); + assertTrue(restored.getColours().isEmpty()); + assertEquals(Material.PAPER, scoop(restored).getType()); + assertNull(env.lastBuilt, "No sauce is served without liquid"); + } + + @Test + void pouringLiquidThatIsNotFoodForgetsAnEarlierSavedLiquid() { + env.variables.put("sauce.liquid", "stale"); + SauceReference sauce = new SauceReference(env.furniture, Method.SAUCEPAN); + env.player.getInventory().setItemInMainHand(new ItemStack(Material.HONEY_BOTTLE)); + sauce.interact(env.interaction()); + + assertEquals(Map.of("liquid", -1), sauce.getSecondaries()); + assertFalse(env.variables.containsKey("sauce.liquid")); + assertEquals("aa5500", env.variables.get("sauce.colours")); + assertEquals(Map.of("liquid", -1), rebuilt().getSecondaries()); + } + + @Test + void missingFurnitureOrTypeLeavesAnEmptySaucepan() { + SauceReference detached = new SauceReference(null, Method.SAUCEPAN); + assertDoesNotThrow(detached::rebuildFromFurniture); + assertDoesNotThrow(detached::clear); + assertTrue(detached.isEmpty()); + org.mockito.Mockito.when(env.furniture.getType()).thenReturn(null); + assertTrue(rebuilt().isEmpty()); + } +}