Skip to content
Merged
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/net/tfminecraft/dowsing/utils/ItemCreator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ version: '${project.version}'
author: Drefvelin

depend: [TLibs, DenarEconomy, SimpleFactions, MMOItems, MythicLib, ItemsAdder]
softdepend: [Magic]
softdepend: [Magic, Cooking]

commands:
dowsing:
Expand Down
6 changes: 3 additions & 3 deletions src/main/resources/production_methods.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
spice_plantation:
weight: 1
prerequisite: basic_farm_irrigation
Expand All @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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));
}
}
}
27 changes: 26 additions & 1 deletion src/test/java/net/tfminecraft/dowsing/utils/ItemCreatorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class ItemCreatorTest {
final Map<Field, Object> previousCache = new HashMap<>();
Set<String> warnings, previousWarnings;
MockedStatic<TLibs> tlibs;
ItemAPI api;
net.tfminecraft.tlibs.objects.api.subapi.ItemCreator provider;
ItemCreator creator;
Node node;
Expand All @@ -52,7 +53,7 @@ void setUp() throws Exception {
Field field = ItemCreator.class.getDeclaredField("loggedInvalidPaths"); field.setAccessible(true);
warnings = (Set<String>) 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));
Expand Down Expand Up @@ -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<String> 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");
Expand Down