diff --git a/README.md b/README.md index 0dbfe58..a30ffc3 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,22 @@ 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:** 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`. 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 [Project documentation](https://github.com/TF-Minecraft/Docs/blob/main/projects/Dowsing/README.md) 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/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..5cf2a8e --- /dev/null +++ b/src/test/java/net/tfminecraft/dowsing/utils/CookingDropPathsTest.java @@ -0,0 +1,55 @@ +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 java.util.Set; + +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(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 { + try (InputStream in = CookingDropPathsTest.class.getClassLoader().getResourceAsStream(name)) { + assertNotNull(in, name); + return YamlConfiguration.loadConfiguration(new InputStreamReader(in, StandardCharsets.UTF_8)); + } + } +} 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");