From 51eddce3a4cc2dd0d9a83fe19290f7391dae8c23 Mon Sep 17 00:00:00 2001 From: XxFran10xX Date: Wed, 11 Nov 2026 13:03:16 +0100 Subject: [PATCH 1/3] Drop Cooking sugar from plantation nodes Cooking turns plain sugar into its sweetener food (with a quality) only when a player picks it up off the ground. Node output that goes straight into the hopper above the node never gets picked up, so it stayed plain: no quality, refused by the sauce pan and the soup pot. The plantation methods now drop Cooking's food path directly (c.sweetener(type=sweetener_1;origin=Sugar)). TLibs hands c. paths to Cooking, so every cycle rolls one quality for its sugar, hopper or not. Cooking is added to softdepend so it is enabled before Dowsing builds its menus. Co-Authored-By: Claude Opus 5.5 --- src/main/resources/plugin.yml | 2 +- src/main/resources/production_methods.yml | 6 +-- .../dowsing/utils/CookingDropPathsTest.java | 53 +++++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 src/test/java/net/tfminecraft/dowsing/utils/CookingDropPathsTest.java diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 16f00a4..7de7a37 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -5,7 +5,7 @@ version: '${project.version}' author: Drefvelin depend: [TLibs, DenarEconomy, SimpleFactions, MMOItems, MythicLib, ItemsAdder] -softdepend: [Magic] +softdepend: [Magic, Cooking] commands: dowsing: diff --git a/src/main/resources/production_methods.yml b/src/main/resources/production_methods.yml index d19e938..b8d7236 100644 --- a/src/main/resources/production_methods.yml +++ b/src/main/resources/production_methods.yml @@ -225,7 +225,7 @@ material_plantation: - time_modifier(-50) - upkeep(5) - add_drop(ia.iasurvival:tobacco,50) - - add_drop(v.sugar,50) + - add_drop(c.sweetener(type=sweetener_1;origin=Sugar),50) spice_plantation: weight: 1 prerequisite: basic_farm_irrigation @@ -236,7 +236,7 @@ spice_plantation: - yield(4) - prestige(4) - upkeep(10) - - add_drop(v.sugar,40) + - add_drop(c.sweetener(type=sweetener_1;origin=Sugar),40) - add_drop(ia.playbox_custom_crops:spiceleaf_seeds,20) - add_drop(ia.playbox_custom_crops:basil_seeds,20) - add_drop(ia.playbox_custom_crops:blackpepper_seeds,20) @@ -250,7 +250,7 @@ sweet_spice_plantation: - yield(4) - prestige(4) - upkeep(10) - - add_drop(v.sugar,40) + - add_drop(c.sweetener(type=sweetener_1;origin=Sugar),40) - add_drop(ia.playbox_custom_crops:vanilla_seeds,20) - add_drop(ia.playbox_custom_crops:cinnamon_seeds,20) - add_drop(ia.playbox_custom_crops:nut_seeds,20) diff --git a/src/test/java/net/tfminecraft/dowsing/utils/CookingDropPathsTest.java b/src/test/java/net/tfminecraft/dowsing/utils/CookingDropPathsTest.java new file mode 100644 index 0000000..8e5f20c --- /dev/null +++ b/src/test/java/net/tfminecraft/dowsing/utils/CookingDropPathsTest.java @@ -0,0 +1,53 @@ +package net.tfminecraft.dowsing.utils; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.Test; + +/** + * Cooking turns plain sugar into food only on a player's ground pickup. Node output that goes + * straight into a hopper never gets picked up, so nodes must drop Cooking's food path directly. + */ +class CookingDropPathsTest { + static final String SUGAR = "c.sweetener(type=sweetener_1;origin=Sugar)"; + + @Test + void cookingFoodPathSurvivesTheAddDropEffectAndTheStoredToken() { + String[] effect = DropPaths.parseAddDropEffect("add_drop(" + SUGAR + ",40)"); + assertArrayEquals(new String[]{SUGAR, "40"}, effect); + + String stored = DropPaths.formatStored(effect[0], Double.parseDouble(effect[1])); + assertEquals(SUGAR + "(40)", stored); + assertArrayEquals(new String[]{SUGAR, "40"}, DropPaths.parseStored(stored)); + } + + @Test + void bundledPlantationsDropCookingSugarInsteadOfPlainSugar() throws Exception { + YamlConfiguration config = bundled("production_methods.yml"); + List sugarDrops = new ArrayList<>(); + for (String key : config.getKeys(false)) { + for (String effect : config.getStringList(key + ".effects")) { + String[] drop = DropPaths.parseAddDropEffect(effect); + if (drop == null) continue; + assertNotEquals("v.sugar", drop[0].toLowerCase(Locale.ROOT), key + " still drops plain sugar"); + if (drop[0].equals(SUGAR)) sugarDrops.add(key); + } + } + assertEquals(List.of("material_plantation", "spice_plantation", "sweet_spice_plantation"), sugarDrops); + } + + static YamlConfiguration bundled(String name) throws Exception { + try (InputStream in = CookingDropPathsTest.class.getClassLoader().getResourceAsStream(name)) { + assertNotNull(in, name); + return YamlConfiguration.loadConfiguration(new InputStreamReader(in, StandardCharsets.UTF_8)); + } + } +} From dcfe40093a8c754798c1e4e91082ef7d6f80a1bd Mon Sep 17 00:00:00 2001 From: XxFran10xX Date: Wed, 11 Nov 2026 13:31:38 +0100 Subject: [PATCH 2/3] Document the manual upgrade for node drop paths Dowsing never rewrites an existing production_methods.yml, so servers upgrading the jar keep v.sugar. Spell out the path swap operators apply by hand, for sugar and for salt. Co-Authored-By: Claude Opus 5.5 --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 0dbfe58..be9fe4f 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,20 @@ nodes that turn inputs, upgrades, and production choices into server resources. Originally created by [Drefvelin](https://github.com/Drefvelin). +## Upgrading + +Dowsing writes its YAML files only when they are missing, so a new jar never changes an +existing server's `production_methods.yml`. Apply default changes listed in the release +notes by hand, then run `dowsing reload` or restart. + +- **Cooking foods from nodes:** a node drop that Cooking turns into food must use Cooking's + `c.` path, not the plain item. Cooking converts plain items only when a player picks + them up off the ground, and node output that goes into the hopper above the node is + never picked up. Replace `add_drop(v.sugar,N)` with + `add_drop(c.sweetener(type=sweetener_1;origin=Sugar),N)` and + `add_drop(ia.tfmc_cooking:salt,N)` with `add_drop(c.salt(type=seasoning_1;origin=Salt),N)`, + keeping each weight `N`. + ## Documentation [Project documentation](https://github.com/TF-Minecraft/Docs/blob/main/projects/Dowsing/README.md) From de90e79c2a6e0036fbf6d53c5ef8d7ee9f592bcc Mon Sep 17 00:00:00 2001 From: XxFran10xX Date: Wed, 11 Nov 2026 13:37:56 +0100 Subject: [PATCH 3/3] Hold a c. drop while Cooking is not loaded TLibs turns a path type nobody registered into dirt, so without Cooking a plantation would have put dirt in the hopper instead of holding the cycle. ItemCreator now treats a c. path as unavailable until Cooking registers its handler, which keeps the existing hold-and-retry and logs it once. Review follow-ups: the README covers every Cooking-converted drop, the hopper slots that mixed qualities take, and the Cooking requirement; the bundled-plantation test compares as a set. Co-Authored-By: Claude Opus 5.5 --- README.md | 14 +++++----- .../dowsing/utils/ItemCreator.java | 7 +++++ .../dowsing/utils/CookingDropPathsTest.java | 4 ++- .../dowsing/utils/ItemCreatorTest.java | 27 ++++++++++++++++++- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index be9fe4f..a30ffc3 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,15 @@ Dowsing writes its YAML files only when they are missing, so a new jar never cha existing server's `production_methods.yml`. Apply default changes listed in the release notes by hand, then run `dowsing reload` or restart. -- **Cooking foods from nodes:** a node drop that Cooking turns into food must use Cooking's - `c.` path, not the plain item. Cooking converts plain items only when a player picks - them up off the ground, and node output that goes into the hopper above the node is - never picked up. Replace `add_drop(v.sugar,N)` with - `add_drop(c.sweetener(type=sweetener_1;origin=Sugar),N)` and +- **Cooking foods from nodes:** any node drop listed in Cooking's `conversions.yml` must use + the Cooking food path from that list (prefixed `c.`), not the plain item. Cooking converts + plain items only when a player picks them up off the ground, and node output that goes + into the hopper above the node is never picked up. For example, replace + `add_drop(v.sugar,N)` with `add_drop(c.sweetener(type=sweetener_1;origin=Sugar),N)` and `add_drop(ia.tfmc_cooking:salt,N)` with `add_drop(c.salt(type=seasoning_1;origin=Salt),N)`, - keeping each weight `N`. + keeping each weight `N`. Each cycle rolls one quality for its food, and different qualities + don't stack, so a hopper that doesn't drain into a chest fills sooner. Cooking must be + loaded: without it a `c.` drop counts as unavailable and the node holds its cycle. ## Documentation diff --git a/src/main/java/net/tfminecraft/dowsing/utils/ItemCreator.java b/src/main/java/net/tfminecraft/dowsing/utils/ItemCreator.java index 540f0bc..692bedc 100644 --- a/src/main/java/net/tfminecraft/dowsing/utils/ItemCreator.java +++ b/src/main/java/net/tfminecraft/dowsing/utils/ItemCreator.java @@ -34,6 +34,13 @@ public ItemStack getItemFromPath(String s) { return null; } try { + // TLibs turns a path type nobody registered into dirt, so a c. drop needs Cooking loaded. + if (s.regionMatches(true, 0, "c.", 0, 2) && TLibs.getItemAPI().getPathHandler("c") == null) { + if (loggedInvalidPaths.add(s)) { + Bukkit.getLogger().warning("[Dowsing] Item path needs Cooking, which is not loaded: " + s); + } + return null; + } return TLibs.getItemAPI().getCreator().getItemFromPath(s); } catch (RuntimeException e) { if (loggedInvalidPaths.add(s)) { diff --git a/src/test/java/net/tfminecraft/dowsing/utils/CookingDropPathsTest.java b/src/test/java/net/tfminecraft/dowsing/utils/CookingDropPathsTest.java index 8e5f20c..5cf2a8e 100644 --- a/src/test/java/net/tfminecraft/dowsing/utils/CookingDropPathsTest.java +++ b/src/test/java/net/tfminecraft/dowsing/utils/CookingDropPathsTest.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Set; import org.bukkit.configuration.file.YamlConfiguration; import org.junit.jupiter.api.Test; @@ -41,7 +42,8 @@ void bundledPlantationsDropCookingSugarInsteadOfPlainSugar() throws Exception { if (drop[0].equals(SUGAR)) sugarDrops.add(key); } } - assertEquals(List.of("material_plantation", "spice_plantation", "sweet_spice_plantation"), sugarDrops); + assertEquals(3, sugarDrops.size(), sugarDrops.toString()); + assertEquals(Set.of("material_plantation", "spice_plantation", "sweet_spice_plantation"), Set.copyOf(sugarDrops)); } static YamlConfiguration bundled(String name) throws Exception { diff --git a/src/test/java/net/tfminecraft/dowsing/utils/ItemCreatorTest.java b/src/test/java/net/tfminecraft/dowsing/utils/ItemCreatorTest.java index 8a8263c..30b4098 100644 --- a/src/test/java/net/tfminecraft/dowsing/utils/ItemCreatorTest.java +++ b/src/test/java/net/tfminecraft/dowsing/utils/ItemCreatorTest.java @@ -39,6 +39,7 @@ class ItemCreatorTest { final Map previousCache = new HashMap<>(); Set warnings, previousWarnings; MockedStatic tlibs; + ItemAPI api; net.tfminecraft.tlibs.objects.api.subapi.ItemCreator provider; ItemCreator creator; Node node; @@ -52,7 +53,7 @@ void setUp() throws Exception { Field field = ItemCreator.class.getDeclaredField("loggedInvalidPaths"); field.setAccessible(true); warnings = (Set) field.get(null); previousWarnings = new HashSet<>(warnings); ItemCreator.clearInvalidPathWarnings(); MockBukkit.mock(); Cache.efficiencyLossPM = 5; Cache.efficiencyLossType = 40; - tlibs = mockStatic(TLibs.class); ItemAPI api = mock(ItemAPI.class); + tlibs = mockStatic(TLibs.class); api = mock(ItemAPI.class); provider = mock(net.tfminecraft.tlibs.objects.api.subapi.ItemCreator.class); tlibs.when(TLibs::getItemAPI).thenReturn(api); when(api.getCreator()).thenReturn(provider); when(provider.getItemFromPath("v.STONE")).thenAnswer(inv -> new ItemStack(Material.STONE)); @@ -90,6 +91,30 @@ void itemResolutionRejectsBlankPathsAndWarnsOncePerBadProviderPath() { } finally { Bukkit.getLogger().removeHandler(handler); } } + @Test + void cookingPathsAreUnavailableUntilCookingRegistersItsHandler() { + // Without a c handler TLibs hands back dirt; the drop must count as unavailable instead. + String sugar = "c.sweetener(type=sweetener_1;origin=Sugar)"; + when(provider.getItemFromPath(sugar)).thenAnswer(inv -> new ItemStack(Material.DIRT)); + List messages = new ArrayList<>(); Handler handler = new Handler() { + @Override public void publish(LogRecord record) { messages.add(record.getMessage()); } + @Override public void flush() {} + @Override public void close() {} + }; + Bukkit.getLogger().addHandler(handler); + try { + assertNull(creator.getItemFromPath(sugar)); assertNull(creator.getItemFromPath("C.sweetener(type=sweetener_1;origin=Sugar)")); + assertNull(creator.getItemFromPath(sugar)); + assertEquals(List.of("[Dowsing] Item path needs Cooking, which is not loaded: " + sugar, + "[Dowsing] Item path needs Cooking, which is not loaded: C.sweetener(type=sweetener_1;origin=Sugar)"), messages); + verify(provider, never()).getItemFromPath(anyString()); + } finally { Bukkit.getLogger().removeHandler(handler); } + when(api.getPathHandler("c")).thenReturn(mock(net.tfminecraft.tlibs.objects.api.subapi.ItemPathHandler.class)); + when(provider.getItemFromPath(sugar)).thenAnswer(inv -> new ItemStack(Material.SUGAR)); + assertEquals(Material.SUGAR, creator.getItemFromPath(sugar).getType()); + assertEquals(Material.STONE, creator.getItemFromPath("v.STONE").getType()); + } + @Test void configuredProductionMenuItemsUseFallbacksAndRenderEffectsCostsAndPrerequisites() throws Exception { YamlConfiguration config = yaml("material: named\nname: Advanced\nmodel_data: 123\n");