diff --git a/src/main/java/net/tfminecraft/cooking/Cooking.java b/src/main/java/net/tfminecraft/cooking/Cooking.java index ff94c83..e778030 100644 --- a/src/main/java/net/tfminecraft/cooking/Cooking.java +++ b/src/main/java/net/tfminecraft/cooking/Cooking.java @@ -1,9 +1,14 @@ package net.tfminecraft.cooking; import java.io.File; +import java.util.List; +import java.util.Map; import java.util.logging.Logger; import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandMap; +import org.bukkit.command.PluginCommand; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; @@ -23,6 +28,7 @@ import net.tfminecraft.cooking.husbandry.HusbandryAnimalsCommand; import net.tfminecraft.cooking.husbandry.HusbandryBreedListener; import net.tfminecraft.cooking.husbandry.HusbandryCareListener; +import net.tfminecraft.cooking.husbandry.HusbandryConfig; import net.tfminecraft.cooking.husbandry.HusbandryDamageListener; import net.tfminecraft.cooking.husbandry.HusbandryDeathListener; import net.tfminecraft.cooking.husbandry.HusbandryHarvestListener; @@ -107,6 +113,7 @@ public class Cooking extends JavaPlugin { private final CropsLoader cropsLoader = new CropsLoader(); private final HusbandryLoader husbandryLoader = new HusbandryLoader(); private HusbandryRepository husbandryRepository; + private boolean husbandryEnabled; private final TagManager tagManager = new TagManager(); private final LegacyFishScan legacyFishScan = new LegacyFishScan(); @@ -152,9 +159,14 @@ public void onEnable() { createConfigs(); loadConfigs(); TLibs.getItemAPI().registerPathHandler("c", CookingPathHandler.INSTANCE); - openHusbandryDatabase(); - HusbandryLifecycleListener.applyStatsRevision(); - net.tfminecraft.cooking.husbandry.HusbandryIdentity.start(this, husbandryRepository); + husbandryEnabled = HusbandryConfig.enabled(); + if (husbandryEnabled) { + openHusbandryDatabase(); + HusbandryLifecycleListener.applyStatsRevision(); + net.tfminecraft.cooking.husbandry.HusbandryIdentity.start(this, husbandryRepository); + } else { + getLogger().info("Husbandry is disabled in husbandry.yml; animals are left to other plugins."); + } registerListeners(); cookingManager.start(); @@ -174,17 +186,23 @@ public void onEnable() { SaturationGuard.start(); NutritionDrainTask.start(); Bukkit.getOnlinePlayers().forEach(NutritionLifecycleListener::initializeCurrent); - HusbandryLifecycleListener.resumeLoadedWorlds(); - HusbandryTickTask.start(); - HusbandryLocator.scanUnloaded(); + if (husbandryEnabled) { + HusbandryLifecycleListener.resumeLoadedWorlds(); + HusbandryTickTask.start(); + HusbandryLocator.scanUnloaded(); + } MMOItemsSupport.claimFirstIfPresent(getServer().getPluginManager(), getLogger()); }); getCommand("cooking").setExecutor(commands); getCommand("cooking").setTabCompleter(commands); - HusbandryAnimalsCommand animals = new HusbandryAnimalsCommand(); - getCommand("animals").setExecutor(animals); - getCommand("animals").setTabCompleter(animals); + if (husbandryEnabled) { + HusbandryAnimalsCommand animals = new HusbandryAnimalsCommand(); + getCommand("animals").setExecutor(animals); + getCommand("animals").setTabCompleter(animals); + } else { + releaseAnimalsCommand(); + } if (ItemScanService.get() != null) { ItemScanService.get().subscribe(tagManager); ItemScanService.get().subscribe(legacyFishScan); @@ -248,8 +266,31 @@ public void reloadAll() { craftingManager.rebuildStations(); NutritionDrainTask.stop(); NutritionDrainTask.start(); - HusbandryTickTask.stop(); - HusbandryTickTask.start(); + if (HusbandryConfig.enabled() != husbandryEnabled) { + getLogger().warning("husbandry.yml enabled changed; restart the server to apply it."); + } + if (husbandryEnabled) { + HusbandryTickTask.stop(); + HusbandryTickTask.start(); + } + } + + /** True when Cooking runs livestock; read once at startup from husbandry.yml {@code enabled}. */ + public boolean isHusbandryEnabled() { + return husbandryEnabled; + } + + /** Frees /animals and /livestock so the plugin that runs livestock can register them. */ + private void releaseAnimalsCommand() { + PluginCommand animals = getCommand("animals"); + CommandMap map = getServer().getCommandMap(); + animals.unregister(map); + Map known = map.getKnownCommands(); + for (String label : List.copyOf(known.keySet())) { + if (known.get(label) == animals) { + known.remove(label); + } + } } // ---------------------------------------------------------------------- @@ -279,16 +320,18 @@ public void registerListeners() { CropCustomCropsBridge.tryRegister(this); getServer().getPluginManager().registerEvents(new CustomFishingBridge(), this); CustomFishingBridge.tryRegister(this); - getServer().getPluginManager().registerEvents(new HusbandryLifecycleListener(), this); - getServer().getPluginManager().registerEvents(new HusbandryCareListener(), this); - getServer().getPluginManager().registerEvents(new HusbandryTamingListener(), this); - getServer().getPluginManager().registerEvents(new HusbandryDeathListener(), this); - getServer().getPluginManager().registerEvents(new HusbandryHarvestListener(), this); - getServer().getPluginManager().registerEvents(new HusbandryBreedListener(), this); - getServer().getPluginManager().registerEvents(new HusbandryNeuterListener(), this); - getServer().getPluginManager().registerEvents(new HusbandryInspectListener(), this); - getServer().getPluginManager().registerEvents(new HusbandryMountListener(), this); - getServer().getPluginManager().registerEvents(new HusbandryDamageListener(), this); + if (husbandryEnabled) { + getServer().getPluginManager().registerEvents(new HusbandryLifecycleListener(), this); + getServer().getPluginManager().registerEvents(new HusbandryCareListener(), this); + getServer().getPluginManager().registerEvents(new HusbandryTamingListener(), this); + getServer().getPluginManager().registerEvents(new HusbandryDeathListener(), this); + getServer().getPluginManager().registerEvents(new HusbandryHarvestListener(), this); + getServer().getPluginManager().registerEvents(new HusbandryBreedListener(), this); + getServer().getPluginManager().registerEvents(new HusbandryNeuterListener(), this); + getServer().getPluginManager().registerEvents(new HusbandryInspectListener(), this); + 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); diff --git a/src/main/java/net/tfminecraft/cooking/api/CookingFoodApi.java b/src/main/java/net/tfminecraft/cooking/api/CookingFoodApi.java new file mode 100644 index 0000000..13aacba --- /dev/null +++ b/src/main/java/net/tfminecraft/cooking/api/CookingFoodApi.java @@ -0,0 +1,111 @@ +package net.tfminecraft.cooking.api; + +import java.util.OptionalInt; + +import org.bukkit.Material; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; + +import net.tfminecraft.cooking.carve.CarvableRoastUtils; +import net.tfminecraft.cooking.carve.CarveSequence; +import net.tfminecraft.cooking.item.FoodItem; +import net.tfminecraft.cooking.loader.CarveSequenceLoader; +import net.tfminecraft.cooking.utils.FoodParser; +import net.tfminecraft.cooking.utils.InventoryAdder; +import net.tfminecraft.cooking.utils.ItemBuilder; +import net.tfminecraft.cooking.utils.ItemUpdater; +import net.tfminecraft.cooking.utils.Keys; +import net.tfminecraft.cooking.utils.QualityUtils; + +/** + * Food that other plugins hand out, such as animal products. Main thread only. + * Quality is the Cooking star rating and is clamped to 1-5. + */ +public final class CookingFoodApi { + + private CookingFoodApi() {} + + /** One food from a Cooking string such as {@code food(type=egg;origin=Egg)}, or null if it is not a food. */ + public static ItemStack buildFood(String food, int quality) { + return build(food, quality, null, -1); + } + + /** + * A roast from a Cooking string such as {@code meat(type=roast;origin=Beef)}, carved down to this many + * meat cuts before its closing bone. The carve sequence floor still applies. Foods without a carve + * sequence are returned whole. Null if the string is not a food. + */ + public static ItemStack buildRoast(String food, int quality, int meatCuts) { + return build(food, quality, null, Math.max(0, meatCuts)); + } + + /** A Cooking milk bucket from a goat, or from a cow for any other animal. Null if milk is not configured. */ + public static ItemStack buildMilk(EntityType origin, int quality) { + return build(milkFood(origin), quality, new ItemStack(Material.MILK_BUCKET), -1); + } + + /** The Cooking string {@link #buildMilk} uses for this animal. */ + public static String milkFood(EntityType origin) { + return origin == EntityType.GOAT + ? "food(type=milk_bucket;origin=Goat)" + : "food(type=milk_bucket;origin=Cow)"; + } + + /** Stars stored on a Cooking item, or empty when the item has no Cooking quality. */ + public static OptionalInt qualityOf(ItemStack stack) { + if (stack == null || stack.getType().isAir() || !stack.hasItemMeta()) { + return OptionalInt.empty(); + } + ItemMeta meta = stack.getItemMeta(); + if (meta == null) { + return OptionalInt.empty(); + } + Integer stored = meta.getPersistentDataContainer().get(Keys.QUALITY, PersistentDataType.INTEGER); + return stored == null ? OptionalInt.empty() : OptionalInt.of(QualityUtils.clamp(stored)); + } + + /** Adds an item the way Cooking stacks food. Returns what did not fit, or null. */ + public static ItemStack addToInventory(Player player, ItemStack item) { + return InventoryAdder.addItem(player, item); + } + + private static ItemStack build(String food, int quality, ItemStack base, int meatCuts) { + if (food == null || food.isBlank()) { + return null; + } + FoodParser.Result parsed = FoodParser.parse(food); + if (parsed == null || parsed.template == null) { + return null; + } + ItemStack stack = ItemBuilder.buildSingleWithQuality(parsed.template, base, QualityUtils.clamp(quality)); + if (meatCuts < 0) { + return stack; + } + FoodItem item = FoodItem.fromItem(stack); + if (item == null) { + return stack; + } + CarvableRoastUtils.readCarveState(item, stack); + String seqId = item.getCarveSequencePdc() != null + ? item.getCarveSequencePdc() + : item.getCarveSequenceId(); + if (seqId == null) { + return stack; + } + CarveSequence seq = CarveSequenceLoader.get(seqId); + if (seq == null) { + return stack; + } + CarvableRoastUtils.RoastPortion portion = CarvableRoastUtils.portion(seq, meatCuts); + item.setCarveState(seqId, portion.nextIndex(), portion.remaining()); + CarvableRoastUtils.writeCarveState(stack, item); + ItemStack updated = ItemUpdater.applyItemUpdate(stack, item, null); + if (updated != null && !updated.getType().isAir()) { + stack = updated; + } + return stack; + } +} diff --git a/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryConfig.java b/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryConfig.java index 2356607..983f0b8 100644 --- a/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryConfig.java +++ b/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryConfig.java @@ -49,6 +49,7 @@ public final class HusbandryConfig { private static double geneticSlowdownDivisor = 1; private static double careInfluence = 0.02; private static String statsRevision = "1"; + private static boolean enabled = true; private static boolean restoreLostAnimals = true; private static List amountBands = List.of(); private static double mountSpeedMinPct = 0.40; @@ -158,6 +159,14 @@ public static void setStatsRevision(String revision) { statsRevision = revision == null ? "" : revision.trim(); } + public static void setEnabled(boolean value) { + enabled = value; + } + + public static boolean enabled() { + return enabled; + } + public static void setRestoreLostAnimals(boolean enabled) { restoreLostAnimals = enabled; } diff --git a/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryFeedQuality.java b/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryFeedQuality.java index 8e02acf..e5af6af 100644 --- a/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryFeedQuality.java +++ b/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryFeedQuality.java @@ -1,14 +1,12 @@ package net.tfminecraft.cooking.husbandry; +import java.util.OptionalInt; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.persistence.PersistentDataType; -import net.tfminecraft.cooking.utils.Keys; -import net.tfminecraft.cooking.utils.QualityUtils; +import net.tfminecraft.cooking.api.CookingFoodApi; /** * Breeding-food stars scale the gene bonus above the parent average. @@ -54,11 +52,11 @@ public static double scaleOf(ItemStack stack) { if (isUniversalFeed(stack)) { return FULL_SCALE; } - Integer stars = starsOrNull(stack); - if (stars == null) { + OptionalInt stars = CookingFoodApi.qualityOf(stack); + if (stars.isEmpty()) { return FULL_SCALE; } - return scaleForStars(stars); + return scaleForStars(stars.getAsInt()); } public static void offer(UUID animalId, double scale, long nowMillis) { @@ -132,20 +130,5 @@ private static boolean isUniversalFeed(ItemStack stack) { } } - private static Integer starsOrNull(ItemStack stack) { - if (stack == null || stack.getType().isAir() || !stack.hasItemMeta()) { - return null; - } - ItemMeta meta = stack.getItemMeta(); - if (meta == null) { - return null; - } - Integer stored = meta.getPersistentDataContainer().get(Keys.QUALITY, PersistentDataType.INTEGER); - if (stored == null) { - return null; - } - return QualityUtils.clamp(stored); - } - private record TimedScale(double scale, long atMillis) {} } diff --git a/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryHarvest.java b/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryHarvest.java index 53e36b6..9f96201 100644 --- a/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryHarvest.java +++ b/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryHarvest.java @@ -11,15 +11,8 @@ import org.bukkit.inventory.ItemStack; import net.tfminecraft.tlibs.TLibs; -import net.tfminecraft.cooking.carve.CarvableRoastUtils; -import net.tfminecraft.cooking.carve.CarveSequence; -import net.tfminecraft.cooking.item.FoodItem; -import net.tfminecraft.cooking.loader.CarveSequenceLoader; -import net.tfminecraft.cooking.utils.FoodParser; +import net.tfminecraft.cooking.api.CookingFoodApi; import net.tfminecraft.cooking.utils.InventoryAdder; -import net.tfminecraft.cooking.utils.ItemBuilder; -import net.tfminecraft.cooking.utils.ItemUpdater; -import net.tfminecraft.cooking.utils.QualityUtils; public final class HusbandryHarvest { @@ -141,17 +134,6 @@ private static void startWoolTimer( } } - public static String milkFoodString(EntityType type) { - return milkFoodStringForType(type == null ? null : type.name()); - } - - static String milkFoodStringForType(String typeName) { - if ("GOAT".equalsIgnoreCase(typeName)) { - return "food(type=milk_bucket;origin=Goat)"; - } - return "food(type=milk_bucket;origin=Cow)"; - } - public static Sound milkSound(EntityType type) { if (type == EntityType.GOAT) { return Sound.ENTITY_GOAT_MILK; @@ -159,48 +141,19 @@ public static Sound milkSound(EntityType type) { return Sound.ENTITY_COW_MILK; } - public static ItemStack buildFood(HusbandryAnimal animal, String foodString) { - return buildFood(animal, foodString, null); - } - public static ItemStack buildMilk(HusbandryAnimal animal, EntityType type) { - return buildFood(animal, milkFoodString(type), new ItemStack(Material.MILK_BUCKET)); - } - - public static ItemStack buildFood(HusbandryAnimal animal, String foodString, ItemStack base) { - if (foodString == null || foodString.isBlank() || animal == null) { + if (animal == null) { return null; } - FoodParser.Result parsed = FoodParser.parse(foodString); - if (parsed == null || parsed.template == null) { + return CookingFoodApi.buildMilk(type, rollQuality(animal, ThreadLocalRandom.current())); + } + + public static ItemStack buildFood(HusbandryAnimal animal, String foodString) { + if (animal == null) { return null; } - int quality = QualityUtils.clamp(rollQuality(animal, ThreadLocalRandom.current())); - ItemStack stack = ItemBuilder.buildSingleWithQuality(parsed.template, base, quality); - FoodItem item = FoodItem.fromItem(stack); - if (item == null) { - return stack; - } - CarvableRoastUtils.readCarveState(item, stack); - String seqId = item.getCarveSequencePdc() != null - ? item.getCarveSequencePdc() - : item.getCarveSequenceId(); - if (seqId == null) { - return stack; - } - CarveSequence seq = CarveSequenceLoader.get(seqId); - if (seq == null) { - return stack; - } int requested = HusbandryConfig.roastCutsFor(HusbandryConfig.effectiveGenetics(animal)); - CarvableRoastUtils.RoastPortion portion = CarvableRoastUtils.portion(seq, requested); - item.setCarveState(seqId, portion.nextIndex(), portion.remaining()); - CarvableRoastUtils.writeCarveState(stack, item); - ItemStack updated = ItemUpdater.applyItemUpdate(stack, item, null); - if (updated != null && !updated.getType().isAir()) { - stack = updated; - } - return stack; + return CookingFoodApi.buildRoast(foodString, rollQuality(animal, ThreadLocalRandom.current()), requested); } public static ItemStack buildTlibs(String path, int amount) { diff --git a/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryLoader.java b/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryLoader.java index 2210e5b..3e4824c 100644 --- a/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryLoader.java +++ b/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryLoader.java @@ -82,6 +82,7 @@ public void load(File file) { config.getDouble("breeding.genetic-slowdown-divisor", 0.4), config.getDouble("breeding.care-influence", 0.02)); HusbandryConfig.setStatsRevision(config.getString("stats-revision", "1")); + HusbandryConfig.setEnabled(config.getBoolean("enabled", true)); HusbandryConfig.setRestoreLostAnimals(config.getBoolean("restore-lost-animals", true)); HusbandryConfig.setProfessionExp( config.getString("profession", "farming"), diff --git a/src/main/java/net/tfminecraft/cooking/manager/CommandManager.java b/src/main/java/net/tfminecraft/cooking/manager/CommandManager.java index a816031..d127a4e 100644 --- a/src/main/java/net/tfminecraft/cooking/manager/CommandManager.java +++ b/src/main/java/net/tfminecraft/cooking/manager/CommandManager.java @@ -199,6 +199,10 @@ private boolean handleHusbandry(CommandSender sender, String[] args) { sender.sendMessage("§cNo permission."); return true; } + if (Cooking.plugin != null && !Cooking.plugin.isHusbandryEnabled()) { + sender.sendMessage("§cHusbandry is disabled in husbandry.yml."); + return true; + } if (args.length >= 2 && args[1].equalsIgnoreCase("save")) { return handleHusbandrySave(sender); } diff --git a/src/main/resources/husbandry.yml b/src/main/resources/husbandry.yml index 3401d62..7a03857 100644 --- a/src/main/resources/husbandry.yml +++ b/src/main/resources/husbandry.yml @@ -1,3 +1,7 @@ +# false hands livestock to another plugin, such as BreedingBuddies: Cooking opens no animal +# database, registers no animal listeners and frees /animals. Read at startup only. +enabled: true + max-animals: 15 care-max: 200 diff --git a/src/test/java/net/tfminecraft/cooking/CookingLifecycleTest.java b/src/test/java/net/tfminecraft/cooking/CookingLifecycleTest.java index c402201..af7aef0 100644 --- a/src/test/java/net/tfminecraft/cooking/CookingLifecycleTest.java +++ b/src/test/java/net/tfminecraft/cooking/CookingLifecycleTest.java @@ -62,6 +62,7 @@ class CookingLifecycleTest { private MockedStatic husbandry; private MockedStatic husbandryTicks; private MockedStatic locator; + private MockedStatic identities; private MockedStatic liquidAging; private MockedStatic churnAging; @TempDir Path directory; @@ -94,7 +95,7 @@ class CookingLifecycleTest { locator = staticMock(HusbandryLocator.class); liquidAging = staticMock(LiquidContainerAging.class); churnAging = staticMock(ButterChurnAging.class); - staticMock(net.tfminecraft.cooking.husbandry.HusbandryIdentity.class); + identities = staticMock(HusbandryIdentity.class); staticMock(CustomFishingCatalog.class); staticMock(CropCustomCropsBridge.class); staticMock(CustomFishingBridge.class); @@ -140,6 +141,7 @@ class CookingLifecycleTest { assertConfigLoaded(plugin, CropsLoader.class, "crops.yml"); assertConfigLoaded(plugin, HusbandryLoader.class, "husbandry.yml"); assertConfigLoaded(plugin, CarveSequenceLoader.class, "carve-sequences.yml"); + assertTrue(plugin.isHusbandryEnabled()); repositories.verify(() -> HusbandryRepository.open(new File(plugin.getDataFolder(), "Data/husbandry.db"))); verify(items).registerPathHandler("c", CookingPathHandler.INSTANCE); assertInstanceOf(CommandManager.class, plugin.getCommand("cooking").getExecutor()); @@ -257,6 +259,47 @@ class CookingLifecycleTest { husbandryTicks.verify(HusbandryTickTask::start, times(2)); } + @Test void disabledHusbandryOpensNoDatabaseRegistersNoAnimalListenersAndFreesAnimals() { + HusbandryConfig.setEnabled(false); + try { + Cooking plugin = loadPlugin(); + + assertFalse(plugin.isHusbandryEnabled()); + assertNull(plugin.getHusbandryRepository()); + repositories.verifyNoInteractions(); + husbandry.verifyNoInteractions(); + identities.verifyNoInteractions(); + assertTrue(HandlerList.getRegisteredListeners(plugin).stream() + .noneMatch(listener -> listener.getListener().getClass().getPackageName() + .equals(HusbandryLifecycleListener.class.getPackageName()))); + assertNotNull(server.getCommandMap().getCommand("cooking")); + for (String label : List.of("animals", "livestock", "cooking:animals", "cooking:livestock")) { + assertNull(server.getCommandMap().getCommand(label), label); + } + + server.getScheduler().performOneTick(); + plugin.reloadAll(); + + husbandryTicks.verifyNoInteractions(); + locator.verifyNoInteractions(); + assertFalse(plugin.isHusbandryEnabled(), "A reload must not switch husbandry on"); + } finally { + HusbandryConfig.setEnabled(true); + } + } + + @Test void reloadWarnsInsteadOfSwitchingHusbandryOffWhileRunning() { + Cooking plugin = loadPlugin(); + HusbandryConfig.setEnabled(false); + try { + plugin.reloadAll(); + assertTrue(plugin.isHusbandryEnabled()); + husbandryTicks.verify(HusbandryTickTask::start, times(1)); + } finally { + HusbandryConfig.setEnabled(true); + } + } + @Test void configGenerationPreservesExistingUserFilesAndFolderCreationIsIdempotent() throws Exception { Cooking plugin = loadPlugin(); Path config = plugin.getDataFolder().toPath().resolve("config.yml"); diff --git a/src/test/java/net/tfminecraft/cooking/api/CookingFoodApiTest.java b/src/test/java/net/tfminecraft/cooking/api/CookingFoodApiTest.java new file mode 100644 index 0000000..daa18f8 --- /dev/null +++ b/src/test/java/net/tfminecraft/cooking/api/CookingFoodApiTest.java @@ -0,0 +1,167 @@ +package net.tfminecraft.cooking.api; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.OptionalInt; +import java.util.UUID; + +import org.bukkit.Material; +import org.bukkit.configuration.MemoryConfiguration; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +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.mockito.MockedStatic; + +import net.tfminecraft.cooking.Cooking; +import net.tfminecraft.cooking.carve.CarveCut; +import net.tfminecraft.cooking.carve.CarveSequence; +import net.tfminecraft.cooking.husbandry.HusbandryAnimal; +import net.tfminecraft.cooking.husbandry.HusbandryHarvest; +import net.tfminecraft.cooking.item.FoodItem; +import net.tfminecraft.cooking.loader.CarveSequenceLoader; +import net.tfminecraft.cooking.utils.FoodParser; +import net.tfminecraft.cooking.utils.InventoryAdder; +import net.tfminecraft.cooking.utils.ItemBuilder; +import net.tfminecraft.cooking.utils.ItemUpdater; +import net.tfminecraft.cooking.utils.Keys; + +class CookingFoodApiTest { + private Cooking previousPlugin; + + @BeforeEach void setUp() { + MockBukkit.mock(); + previousPlugin = Cooking.plugin; + Cooking.plugin = mock(Cooking.class); + when(Cooking.plugin.getName()).thenReturn("Cooking"); + when(Cooking.plugin.namespace()).thenReturn("cooking"); + } + + @AfterEach void tearDown() { + MockBukkit.unmock(); + Cooking.plugin = previousPlugin; + } + + @Test void milkComesFromGoatsOrOtherwiseCows() { + assertEquals("food(type=milk_bucket;origin=Goat)", CookingFoodApi.milkFood(EntityType.GOAT)); + assertEquals("food(type=milk_bucket;origin=Cow)", CookingFoodApi.milkFood(EntityType.COW)); + assertEquals("food(type=milk_bucket;origin=Cow)", CookingFoodApi.milkFood(null)); + } + + @Test void foodAndMilkAreBuiltWholeAtClampedStars() { + FoodItem egg = food("egg"); + ItemStack built = new ItemStack(Material.EGG); + try (MockedStatic parser = mockStatic(FoodParser.class); + MockedStatic builder = mockStatic(ItemBuilder.class); + MockedStatic codec = mockStatic(FoodItem.class)) { + parser.when(() -> FoodParser.parse(anyString())).thenReturn(parsed(egg)); + builder.when(() -> ItemBuilder.buildSingleWithQuality(eq(egg), any(), anyInt())).thenReturn(built); + + assertSame(built, CookingFoodApi.buildFood("food(type=egg)", 9)); + builder.verify(() -> ItemBuilder.buildSingleWithQuality(egg, null, 5)); + assertSame(built, CookingFoodApi.buildMilk(EntityType.GOAT, 0)); + parser.verify(() -> FoodParser.parse("food(type=milk_bucket;origin=Goat)")); + builder.verify(() -> ItemBuilder.buildSingleWithQuality(eq(egg), + argThat(base -> base != null && base.getType() == Material.MILK_BUCKET), eq(1))); + codec.verifyNoInteractions(); + } + } + + @Test void roastsNeverAskForFewerThanZeroCuts() { + FoodItem roast = food("roast"); + roast.setCarveState("roast_seq", 0, 3); + ItemStack built = new ItemStack(Material.BEEF); + CarveSequence sequence = new CarveSequence("roast_seq", 1, List.of(CarveCut.food("slice", 4, 4), + CarveCut.food("slice", 4, 4), CarveCut.item("bone", 1, 0, 0))); + try (MockedStatic parser = mockStatic(FoodParser.class); + MockedStatic builder = mockStatic(ItemBuilder.class); + MockedStatic codec = mockStatic(FoodItem.class, CALLS_REAL_METHODS); + MockedStatic sequences = mockStatic(CarveSequenceLoader.class); + MockedStatic updater = mockStatic(ItemUpdater.class)) { + parser.when(() -> FoodParser.parse("roast")).thenReturn(parsed(roast)); + builder.when(() -> ItemBuilder.buildSingleWithQuality(eq(roast), any(), anyInt())).thenReturn(built); + codec.when(() -> FoodItem.fromItem(built)).thenReturn(roast); + sequences.when(() -> CarveSequenceLoader.get("roast_seq")).thenReturn(sequence); + + CookingFoodApi.buildRoast("roast", 3, -4); + + assertEquals(2, roast.getCarveRemaining(), "The sequence floor keeps one cut and the bone"); + } + } + + @Test void roastsKeepThePositiveNumberOfCutsAskedFor() { + FoodItem roast = food("roast"); + roast.setCarveState("roast_seq", 0, 3); + ItemStack built = new ItemStack(Material.BEEF); + CarveSequence sequence = new CarveSequence("roast_seq", 1, List.of(CarveCut.food("slice", 4, 4), + CarveCut.food("slice", 4, 4), CarveCut.item("bone", 1, 0, 0))); + try (MockedStatic parser = mockStatic(FoodParser.class); + MockedStatic builder = mockStatic(ItemBuilder.class); + MockedStatic codec = mockStatic(FoodItem.class, CALLS_REAL_METHODS); + MockedStatic sequences = mockStatic(CarveSequenceLoader.class); + MockedStatic updater = mockStatic(ItemUpdater.class)) { + parser.when(() -> FoodParser.parse("roast")).thenReturn(parsed(roast)); + builder.when(() -> ItemBuilder.buildSingleWithQuality(eq(roast), any(), anyInt())).thenReturn(built); + codec.when(() -> FoodItem.fromItem(built)).thenReturn(roast); + sequences.when(() -> CarveSequenceLoader.get("roast_seq")).thenReturn(sequence); + + CookingFoodApi.buildRoast("roast", 3, 2); + + assertEquals(3, roast.getCarveRemaining(), "Two meat cuts and the bone"); + } + } + + @Test void qualityIsReadOnlyFromCookingItems() { + assertEquals(OptionalInt.empty(), CookingFoodApi.qualityOf(null)); + assertEquals(OptionalInt.empty(), CookingFoodApi.qualityOf(new ItemStack(Material.AIR))); + assertEquals(OptionalInt.empty(), CookingFoodApi.qualityOf(new ItemStack(Material.WHEAT))); + ItemStack named = new ItemStack(Material.WHEAT); + ItemMeta meta = named.getItemMeta(); + meta.setDisplayName("Wheat"); + named.setItemMeta(meta); + assertEquals(OptionalInt.empty(), CookingFoodApi.qualityOf(named)); + meta.getPersistentDataContainer().set(Keys.QUALITY, PersistentDataType.INTEGER, 7); + named.setItemMeta(meta); + assertEquals(OptionalInt.of(5), CookingFoodApi.qualityOf(named)); + } + + @Test void inventoryAdditionUsesCookingFoodStacking() { + Player player = mock(Player.class); + ItemStack item = new ItemStack(Material.EGG); + ItemStack leftover = new ItemStack(Material.EGG, 2); + try (MockedStatic adder = mockStatic(InventoryAdder.class)) { + adder.when(() -> InventoryAdder.addItem(player, item)).thenReturn(leftover); + assertSame(leftover, CookingFoodApi.addToInventory(player, item)); + } + } + + @Test void husbandryMilksThroughTheApiOnlyWithAnAnimal() { + assertNull(HusbandryHarvest.buildMilk(null, EntityType.COW)); + try (MockedStatic api = mockStatic(CookingFoodApi.class)) { + ItemStack milk = new ItemStack(Material.MILK_BUCKET); + api.when(() -> CookingFoodApi.buildMilk(eq(EntityType.GOAT), anyInt())).thenReturn(milk); + assertSame(milk, HusbandryHarvest.buildMilk(new HusbandryAnimal(UUID.randomUUID(), "GOAT", "Nanny"), EntityType.GOAT)); + } + } + + private static FoodItem food(String id) { + MemoryConfiguration config = new MemoryConfiguration(); + config.set("name", id); + config.set("update", false); + return new FoodItem(id, config); + } + + private static FoodParser.Result parsed(FoodItem template) { + FoodParser.Result result = new FoodParser.Result(); + result.template = template; + return result; + } +} diff --git a/src/test/java/net/tfminecraft/cooking/husbandry/HusbandryLoaderCoverageTest.java b/src/test/java/net/tfminecraft/cooking/husbandry/HusbandryLoaderCoverageTest.java index 733ed54..eee6d12 100644 --- a/src/test/java/net/tfminecraft/cooking/husbandry/HusbandryLoaderCoverageTest.java +++ b/src/test/java/net/tfminecraft/cooking/husbandry/HusbandryLoaderCoverageTest.java @@ -47,6 +47,7 @@ class HusbandryLoaderCoverageTest { breeding: {genetic-variance-multiplier: 0.3, genetic-slowdown-divisor: 0.7, care-influence: 0.04} stats-revision: ' new-revision ' restore-lost-animals: false + enabled: false profession: ' ranching ' exp: {min: 4, max: 9} species: @@ -103,7 +104,7 @@ class HusbandryLoaderCoverageTest { assertEquals(Set.of(EntityType.COW, EntityType.PIG), HusbandryConfig.removeUnowned()); assertTrue(HusbandryConfig.isRemoveUnowned(EntityType.COW)); assertFalse(HusbandryConfig.isRemoveUnowned(null)); assertFalse(HusbandryConfig.isRemoveUnowned(EntityType.WOLF)); assertFalse(HusbandryConfig.damageOtherPlayers()); assertFalse(HusbandryConfig.damageOwner()); assertFalse(HusbandryConfig.damageMobs()); assertFalse(HusbandryConfig.damageEnvironment()); assertEquals(0.3, HusbandryConfig.geneticVarianceMultiplier()); assertEquals(0.7, HusbandryConfig.geneticSlowdownDivisor()); assertEquals(0.04, HusbandryConfig.careInfluence()); - assertEquals("new-revision", HusbandryConfig.statsRevision()); assertFalse(HusbandryConfig.restoreLostAnimals()); assertEquals("ranching", HusbandryConfig.professionId()); + assertEquals("new-revision", HusbandryConfig.statsRevision()); assertFalse(HusbandryConfig.restoreLostAnimals()); assertFalse(HusbandryConfig.enabled()); assertEquals("ranching", HusbandryConfig.professionId()); assertEquals(0.1, HusbandryConfig.mountSpeedMinPct()); assertEquals(0.2, HusbandryConfig.mountSpeedGeneticsPct()); assertEquals(0.3, HusbandryConfig.mountSpeedCarePct()); assertEquals(Set.of(EntityType.COW, EntityType.WOLF), HusbandryConfig.species().keySet()); HusbandrySpecies cow = HusbandryConfig.species(EntityType.COW); diff --git a/src/test/java/net/tfminecraft/cooking/husbandry/HusbandryProductsTest.java b/src/test/java/net/tfminecraft/cooking/husbandry/HusbandryProductsTest.java index cdf514b..4be37f5 100644 --- a/src/test/java/net/tfminecraft/cooking/husbandry/HusbandryProductsTest.java +++ b/src/test/java/net/tfminecraft/cooking/husbandry/HusbandryProductsTest.java @@ -66,13 +66,6 @@ void cowListsSlaughterAndMilk() { assertEquals(List.of("On slaughter", "Milk"), HusbandryProducts.modeLines(species)); } - @Test - void milkFoodStringUsesAnimalOrigin() { - assertEquals("food(type=milk_bucket;origin=Goat)", HusbandryHarvest.milkFoodStringForType("GOAT")); - assertEquals("food(type=milk_bucket;origin=Cow)", HusbandryHarvest.milkFoodStringForType("COW")); - assertEquals("food(type=milk_bucket;origin=Cow)", HusbandryHarvest.milkFoodStringForType("PIG")); - } - private static HusbandrySpecies species( boolean milk, String slaughterMeat, diff --git a/src/test/java/net/tfminecraft/cooking/manager/CommandManagerCoverageTest.java b/src/test/java/net/tfminecraft/cooking/manager/CommandManagerCoverageTest.java index 5592db8..b26d14a 100644 --- a/src/test/java/net/tfminecraft/cooking/manager/CommandManagerCoverageTest.java +++ b/src/test/java/net/tfminecraft/cooking/manager/CommandManagerCoverageTest.java @@ -71,6 +71,7 @@ class CommandManagerCoverageTest { player.setOp(true); previousPlugin = Cooking.plugin; plugin = mock(Cooking.class); + when(plugin.isHusbandryEnabled()).thenReturn(true); Cooking.plugin = plugin; identities = new net.tfminecraft.cooking.testsupport.TestCharacters(plugin); InteractibleFurniture integration = mock(InteractibleFurniture.class); @@ -250,6 +251,17 @@ private void ready(PlayerMock owner, RPCharacter character) { assertEquals(List.of("§cCare must be a number."), messages()); } + @Test void husbandryCommandsExplainWhenAnotherPluginRunsLivestock() { + when(plugin.isHusbandryEnabled()).thenReturn(false); + MockedStatic spawner = staticMock(HusbandrySpawner.class); + run("husbandry", "spawn", "cow"); + assertEquals(List.of("§cHusbandry is disabled in husbandry.yml."), messages()); + run("husbandry", "save"); + assertEquals(List.of("§cHusbandry is disabled in husbandry.yml."), messages()); + spawner.verifyNoInteractions(); + verify(plugin, never()).getHusbandryRepository(); + } + @Test void husbandrySpawnUsesDefaultsAndReportsClampedGeneticsAndCare() { MockedStatic config = staticMock(HusbandryConfig.class); config.when(() -> HusbandryConfig.species(EntityType.COW)).thenReturn(mock(HusbandrySpecies.class));