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
11 changes: 11 additions & 0 deletions src/main/java/world/bentobox/limits/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ enum GeneralGroup {
private final boolean logLimitsOnJoin;
private final boolean asyncGolums;
private final boolean showLimitMessages;
private final boolean stackedPlantsCountAsOne;
private static final List<EntityType> DISALLOWED = Arrays.asList(
EntityType.TNT,
EntityType.EVOKER_FANGS,
Expand Down Expand Up @@ -96,6 +97,8 @@ public Settings(Limits addon) {
asyncGolums = addon.getConfig().getBoolean("async-golums", true);
// Show or suppress the "hit the limit" player notifications
showLimitMessages = addon.getConfig().getBoolean("show-limit-messages", true);
// Count a stackable plant column (sugar cane, bamboo) as a single plant
stackedPlantsCountAsOne = addon.getConfig().getBoolean("stacked-plants-count-as-one", false);

addon.log("Entity limits:");
envLimits.forEach((env, m) -> m.entrySet().stream()
Expand Down Expand Up @@ -266,6 +269,14 @@ public boolean isShowLimitMessages() {
return showLimitMessages;
}

/**
* @return true if a column of stackable plants (sugar cane, bamboo) counts as one
* plant; false if every segment counts (default)
*/
public boolean isStackedPlantsCountAsOne() {
return stackedPlantsCountAsOne;
}

public Map<GeneralGroup, Integer> getGeneral() {
return general;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,20 @@ private void scanAsync(Environment env, Chunk chunk) {
}

private void scanColumn(Environment env, ChunkSnapshot chunkSnapshot, int x, int z, int minY, int maxY) {
boolean stackedAsOne = addon.getSettings().isStackedPlantsCountAsOne();
NamespacedKey below = null;
for (int y = minY; y < maxY; y++) {
BlockData blockData = chunkSnapshot.getBlockData(x, y, z);
if (Tag.SLABS.isTagged(blockData.getMaterial())
&& ((Slab) blockData).getType().equals(Slab.Type.DOUBLE)) {
checkBlock(env, blockData);
}
checkBlock(env, blockData);
NamespacedKey key = bll.fixMaterial(blockData);
// Stacked-plants-as-one: segments sitting on the same plant are not counted
if (!(stackedAsOne && BlockLimitsListener.STACKABLE.contains(key) && key.equals(below))) {
checkBlock(env, blockData);
}
below = key;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ public class BlockLimitsListener implements Listener {
private static final List<NamespacedKey> DO_NOT_COUNT = List.of(Material.LAVA.getKey(), Material.WATER.getKey(),
Material.AIR.getKey(), Material.FIRE.getKey(), Material.END_PORTAL.getKey(),
Material.NETHER_PORTAL.getKey());
private static final List<NamespacedKey> STACKABLE = List.of(Material.SUGAR_CANE.getKey(), Material.BAMBOO.getKey());
/** Plants that grow as a vertical column on top of themselves. */
public static final List<NamespacedKey> STACKABLE = List.of(Material.SUGAR_CANE.getKey(), Material.BAMBOO.getKey());

/*
* Materials added in Minecraft 1.21.9 ("Copper Age"). Resolved by name so the
Expand Down Expand Up @@ -269,7 +270,9 @@ private void handleBreak(Block b) {
return;
}
Material mat = b.getType();
if (STACKABLE.contains(b.getType().getKey())) {
// When stacked plants count as one, only the base segment was ever counted,
// so the stems above must not be decremented here.
if (!addon.getSettings().isStackedPlantsCountAsOne() && STACKABLE.contains(b.getType().getKey())) {
Block block = b;
while (block.getRelative(BlockFace.UP).getType().equals(mat)
&& block.getY() < b.getWorld().getMaxHeight()) {
Expand Down Expand Up @@ -407,6 +410,11 @@ public void onBlock(BlockFromToEvent e) {
}
}

/** True if the given material normalises to the same stackable plant key. */
private static boolean isSamePlant(Material below, NamespacedKey plantKey) {
return VARIANT_MAP.getOrDefault(below, below).getKey().equals(plantKey);
}

/**
* Map variant materials to their canonical form.
*/
Expand All @@ -432,6 +440,12 @@ private int process(Block b, BlockData blockData, boolean add) {
if (DO_NOT_COUNT.contains(fixMaterial(blockData)) || !addon.inGameModeWorld(b.getWorld())) {
return -1;
}
// Stacked-plants-as-one: a segment sitting on the same plant is not counted,
// limited, or decremented — only the base segment represents the plant.
if (addon.getSettings().isStackedPlantsCountAsOne() && STACKABLE.contains(fixMaterial(blockData))
&& isSamePlant(b.getRelative(BlockFace.DOWN).getType(), fixMaterial(blockData))) {
return -1;
}
Environment env = envOf(b.getWorld());
return addon.getIslands().getIslandAt(b.getLocation()).map(i -> {
String id = i.getUniqueId();
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ show-limit-messages: true
blocklimits:
HOPPER: 10

# Count stackable plants (SUGAR_CANE, BAMBOO) as a single plant no matter how tall
# they grow. When false (default), every segment of the plant counts toward the limit.
# Run a recount (/<gamemode> limits recount) after changing this so stored counts
# match the new counting rule.
stacked-plants-count-as-one: false

# Override block limits in the nether for this game mode.
# Uncomment and add entries to set nether-only limits.
#blocklimits-nether:
Expand Down
12 changes: 12 additions & 0 deletions src/test/java/world/bentobox/limits/SettingsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ void testShowLimitMessagesDisabled() {
assertFalse(s.isShowLimitMessages());
}

@Test
void testStackedPlantsCountAsOneDefaultsFalse() {
assertFalse(settings.isStackedPlantsCountAsOne());
}

@Test
void testStackedPlantsCountAsOneEnabled() {
config.set("stacked-plants-count-as-one", true);
Settings s = new Settings(addon);
assertTrue(s.isStackedPlantsCountAsOne());
}

@Test
void testGetGeneralEmpty() {
// Default config.yml does not have ANIMALS or MOBS entries in entitylimits
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ void setUp() throws Exception {
doAnswer(invocation -> null).when(addon).log(anyString());
doAnswer(invocation -> null).when(addon).logError(anyString());
when(addon.isCoveredGameMode(anyString())).thenReturn(true);
// Limits settings: stacked-plants defaults false (mock default), messages on
when(addon.getSettings()).thenReturn(limitsSettings);
when(limitsSettings.isShowLimitMessages()).thenReturn(true);
when(addon.inGameModeWorld(any(World.class))).thenReturn(false);
Expand Down Expand Up @@ -966,6 +967,61 @@ void testEntityChangeBlockFarmlandRemovesCropAbove() {
assertEquals(1, ibc.getBlockCount(Material.DIRT.getKey()));
}

// --- Stacked plants count as one (#53) ---

@Test
void testStackedPlantSegmentNotCountedWhenEnabled() {
when(limitsSettings.isStackedPlantsCountAsOne()).thenReturn(true);
Block below = mockBlock(Material.SUGAR_CANE, new Location(world, 100, 64, 100));
Block block = mockBlock(Material.SUGAR_CANE, blockLocation);
when(block.getRelative(BlockFace.DOWN)).thenReturn(below);

BlockState replacedState = mock(BlockState.class);
BlockPlaceEvent event = new BlockPlaceEvent(block, replacedState, block,
new ItemStack(Material.SUGAR_CANE), player, true, EquipmentSlot.HAND);
listener.onBlock(event);

assertFalse(event.isCancelled());
IslandBlockCount ibc = listener.getIsland("test-island-id");
assertTrue(ibc == null || ibc.getBlockCount(Material.SUGAR_CANE.getKey()) == 0);
}

@Test
void testStackedPlantBaseCountedWhenEnabled() {
when(limitsSettings.isStackedPlantsCountAsOne()).thenReturn(true);
Block block = mockBlock(Material.SUGAR_CANE, blockLocation);
// Below is AIR from the mockBlock helper — this is the base segment

BlockState replacedState = mock(BlockState.class);
BlockPlaceEvent event = new BlockPlaceEvent(block, replacedState, block,
new ItemStack(Material.SUGAR_CANE), player, true, EquipmentSlot.HAND);
listener.onBlock(event);

assertEquals(1, listener.getIsland("test-island-id").getBlockCount(Material.SUGAR_CANE.getKey()));
}

@Test
void testStackedPlantBreakBaseDecrementsOnlyOneWhenEnabled() {
when(limitsSettings.isStackedPlantsCountAsOne()).thenReturn(true);
// Only the base was ever counted
IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock");
ibc.add(Environment.NORMAL, Material.SUGAR_CANE.getKey());
listener.setIsland("test-island-id", ibc);

when(world.getMaxHeight()).thenReturn(320);
Block bottomBlock = mockBlock(Material.SUGAR_CANE, new Location(world, 100, 65, 100));
when(bottomBlock.getY()).thenReturn(65);
Block midBlock = mockBlock(Material.SUGAR_CANE, new Location(world, 100, 66, 100));
when(midBlock.getY()).thenReturn(66);
when(bottomBlock.getRelative(BlockFace.UP)).thenReturn(midBlock);

BlockBreakEvent event = new BlockBreakEvent(bottomBlock, player);
listener.onBlock(event);

// Exactly the one counted base is removed; the stem walk must not run
assertEquals(0, listener.getIsland("test-island-id").getBlockCount(Material.SUGAR_CANE.getKey()));
}

// --- Block cascade tests ---

@Test
Expand Down
Loading