Skip to content
Closed
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Cooking makes food an interactive part of everyday life. Players grow and gather

## A complete food journey

Crops are planted outdoors under open sky or beneath the glass roof of a greenhouse. A harvest can become flour, dough, bread, or part of a cooked dish. Milk and meat enter their own preparation chains, and the result can be served through furniture as well as carried as food items. Cooking connects these activities into a shared system for farmers, cooks, and diners.
Crops are planted outdoors under open sky, beneath the glass roof of a greenhouse, or under a low pergola of slabs, stairs and signs. A harvest can become flour, dough, bread, or part of a cooked dish. Milk and meat enter their own preparation chains, and the result can be served through furniture as well as carried as food items. Cooking connects these activities into a shared system for farmers, cooks, and diners.

## Documentation

Expand Down
53 changes: 43 additions & 10 deletions src/main/java/net/tfminecraft/cooking/crops/CropPlantingRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
import java.util.HashSet;
import java.util.Locale;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Tag;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.type.Slab;
import org.bukkit.configuration.ConfigurationSection;

/** Food crops need an open column, with configurable greenhouse covers. */
Expand All @@ -20,34 +25,53 @@ public final class CropPlantingRule {
private static boolean enabled = true;
private static boolean allowGlass = true;
private static Set<Material> allowedCover = Set.of();
private static Set<Material> lowCover = Set.of();
private static int lowCoverHeight = 0;
private static Set<Material> exemptVanilla = Set.of(Material.NETHER_WART, Material.BROWN_MUSHROOM, Material.RED_MUSHROOM);
private static Set<String> exemptCustom = Set.of("yeast");

public static void configure(ConfigurationSection section) {
enabled = section == null || section.getBoolean("require-open-sky", true);
allowGlass = section == null || section.getBoolean("allow-glass-roofs", true);
allowedCover = materials(section, "allowed-cover", Set.of());
exemptVanilla = materials(section, "exempt-vanilla", Set.of(Material.NETHER_WART, Material.BROWN_MUSHROOM, Material.RED_MUSHROOM));
allowedCover = materials(section, "allowed-cover", "allowed-cover", Set.of());
ConfigurationSection low = section == null ? null : section.getConfigurationSection("low-cover");
lowCover = materials(low, "materials", "low-cover.materials", Set.of());
lowCoverHeight = low == null ? 0 : Math.max(0, low.getInt("max-height", 5));
exemptVanilla = materials(section, "exempt-vanilla", "exempt-vanilla", Set.of(Material.NETHER_WART, Material.BROWN_MUSHROOM, Material.RED_MUSHROOM));
exemptCustom = section == null || !section.contains("exempt-custom") ? Set.of("yeast")
: Set.copyOf(section.getStringList("exempt-custom").stream().map(id -> id.toLowerCase(Locale.ROOT)).toList());
}

private static Set<Material> materials(ConfigurationSection section, String key, Set<Material> defaults) {
private static Set<Material> materials(ConfigurationSection section, String key, String label, Set<Material> defaults) {
if (section == null || !section.contains(key)) {
return defaults;
}
Set<Material> result = new HashSet<>();
for (String name : section.getStringList(key)) {
Material material = Material.matchMaterial(name);
if (material != null && material.isBlock()) {
result.add(material);
if (name.startsWith("#")) {
Tag<Material> tag = blockTag(name.substring(1));
if (tag != null) {
result.addAll(tag.getValues());
continue;
}
} else {
org.bukkit.Bukkit.getLogger().warning("[Cooking] Invalid planting material in " + key + ": " + name);
Material material = Material.matchMaterial(name);
if (material != null && material.isBlock()) {
result.add(material);
continue;
}
}
Bukkit.getLogger().warning("[Cooking] Invalid planting material in " + label + ": " + name);
}
return Set.copyOf(result);
}

/** {@code #slabs} or {@code #minecraft:slabs}; null when the tag is unknown. */
private static Tag<Material> blockTag(String name) {
NamespacedKey key = name.isEmpty() ? null : NamespacedKey.fromString(name.toLowerCase(Locale.ROOT));
return key == null ? null : Bukkit.getTag(Tag.REGISTRY_BLOCKS, key, Material.class);
}

private CropPlantingRule() {}

public static boolean requiresOpenSky(Material material) {
Expand All @@ -65,13 +89,22 @@ public static boolean hasOpenSky(Location location) {
if (world == null) {
return false;
}
for (int y = location.getBlockY() + 1; y < world.getMaxHeight(); y++) {
Material cover = world.getBlockAt(location.getBlockX(), y, location.getBlockZ()).getType();
int crop = location.getBlockY();
for (int y = crop + 1; y < world.getMaxHeight(); y++) {
Block block = world.getBlockAt(location.getBlockX(), y, location.getBlockZ());
Material cover = block.getType();
if (!cover.isAir() && !allowedCover.contains(cover)
&& !(allowGlass && (cover.name().endsWith("GLASS") || cover.name().endsWith("GLASS_PANE")))) {
&& !(allowGlass && (cover.name().endsWith("GLASS") || cover.name().endsWith("GLASS_PANE")))
&& !(y - crop <= lowCoverHeight && isLowCover(block, cover))) {
return false;
}
}
return true;
}

/** Low decoration such as slabs, stairs and signs; a double slab is a full block, so it stays a roof. */
private static boolean isLowCover(Block block, Material cover) {
return lowCover.contains(cover)
&& !(block.getBlockData() instanceof Slab slab && slab.getType() == Slab.Type.DOUBLE);
}
}
5 changes: 5 additions & 0 deletions src/main/resources/crops.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ planting:
allow-glass-roofs: true
# Additional Bukkit block materials allowed anywhere above the crop.
allowed-cover: []
# Decoration allowed only within max-height blocks above the crop (pergolas, lantern posts).
# "#name" entries are vanilla block tags. Double slabs are full blocks and still count as a roof.
low-cover:
max-height: 5
materials: ["#slabs", "#stairs", "#all_signs"]
exempt-vanilla: [NETHER_WART, BROWN_MUSHROOM, RED_MUSHROOM]
exempt-custom: [yeast]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

import java.util.List;
import java.util.Set;
import java.util.logging.Logger;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Tag;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.type.Slab;
import org.bukkit.entity.Player;
import org.bukkit.event.block.BlockPlaceEvent;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -16,6 +24,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.mockito.MockedStatic;

import net.momirealms.customcrops.api.core.mechanic.crop.CropConfig;
import net.momirealms.customcrops.api.event.CropPlantEvent;
Expand Down Expand Up @@ -96,10 +105,11 @@ void wartMushroomsAndBuildingBlocksAreExempt() {
assertTrue(CropPlantingRule.customRequiresOpenSky("rice"));
}

private void roof(Material material, int y) {
private Block roof(Material material, int y) {
Block block = mock(Block.class);
when(block.getType()).thenReturn(material);
when(world.getBlockAt(4, y, 8)).thenReturn(block);
return block;
}

@AfterEach
Expand Down Expand Up @@ -136,4 +146,71 @@ void configurableCoverExemptionsAndDisable() {
assertFalse(CropPlantingRule.requiresOpenSky(Material.CARROTS));
assertFalse(CropPlantingRule.customRequiresOpenSky("rice"));
}

@Test
void lowCoverCountsOnlyWithinItsHeight() {
YamlConfiguration config = new YamlConfiguration();
config.set("low-cover.max-height", 5);
config.set("low-cover.materials", List.of("OAK_SLAB", "OAK_STAIRS", "OAK_SIGN"));
CropPlantingRule.configure(config);
roof(Material.OAK_SIGN, 65);
roof(Material.OAK_STAIRS, 67);
roof(Material.OAK_SLAB, 69);
assertTrue(CropPlantingRule.hasOpenSky(location));
roof(Material.OAK_SLAB, 70);
assertFalse(CropPlantingRule.hasOpenSky(location));
roof(Material.AIR, 70);
roof(Material.STONE, 90);
assertFalse(CropPlantingRule.hasOpenSky(location));
}

@Test
void doubleSlabsStayARoofAndMissingLowCoverChangesNothing() {
YamlConfiguration config = new YamlConfiguration();
config.set("low-cover.materials", List.of("OAK_SLAB"));
CropPlantingRule.configure(config);
Block slab = roof(Material.OAK_SLAB, 66);
Slab data = mock(Slab.class);
when(slab.getBlockData()).thenReturn(data);
when(data.getType()).thenReturn(Slab.Type.TOP);
assertTrue(CropPlantingRule.hasOpenSky(location), "max-height defaults to 5");
when(data.getType()).thenReturn(Slab.Type.DOUBLE);
assertFalse(CropPlantingRule.hasOpenSky(location));

CropPlantingRule.configure(new YamlConfiguration());
when(data.getType()).thenReturn(Slab.Type.BOTTOM);
assertFalse(CropPlantingRule.hasOpenSky(location));
config.set("low-cover.max-height", -3);
CropPlantingRule.configure(config);
assertFalse(CropPlantingRule.hasOpenSky(location));
}

@Test
@SuppressWarnings("unchecked")
void lowCoverReadsBlockTagsAndWarnsOnUnknownEntries() {
Tag<Material> slabs = mock(Tag.class);
when(slabs.getValues()).thenReturn(Set.of(Material.OAK_SLAB, Material.STONE_SLAB));
Tag<Material> signs = mock(Tag.class);
when(signs.getValues()).thenReturn(Set.of(Material.OAK_SIGN, Material.OAK_WALL_HANGING_SIGN));
Logger logger = mock(Logger.class);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
bukkit.when(Bukkit::getLogger).thenReturn(logger);
bukkit.when(() -> Bukkit.getTag(Tag.REGISTRY_BLOCKS, NamespacedKey.minecraft("slabs"), Material.class))
.thenReturn(slabs);
bukkit.when(() -> Bukkit.getTag(Tag.REGISTRY_BLOCKS, NamespacedKey.minecraft("all_signs"), Material.class))
.thenReturn(signs);
YamlConfiguration config = new YamlConfiguration();
config.set("low-cover.materials", List.of("#SLABS", "#minecraft:all_signs", "#nope", "#", "#bad key", "DIAMOND"));
CropPlantingRule.configure(config);
verify(logger).warning("[Cooking] Invalid planting material in low-cover.materials: #nope");
verify(logger).warning("[Cooking] Invalid planting material in low-cover.materials: #");
verify(logger).warning("[Cooking] Invalid planting material in low-cover.materials: #bad key");
verify(logger).warning("[Cooking] Invalid planting material in low-cover.materials: DIAMOND");
}
roof(Material.STONE_SLAB, 65);
roof(Material.OAK_WALL_HANGING_SIGN, 66);
assertTrue(CropPlantingRule.hasOpenSky(location));
roof(Material.OAK_STAIRS, 67);
assertFalse(CropPlantingRule.hasOpenSky(location));
}
}
Loading