Skip to content

Commit 7255a2f

Browse files
authored
Merge pull request #285 from BentoBoxWorld/feat/53-stackable-plants
Add option to count stackable plants as a single plant
2 parents 840bbe7 + 06ee8eb commit 7255a2f

6 files changed

Lines changed: 109 additions & 3 deletions

File tree

src/main/java/world/bentobox/limits/Settings.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ enum GeneralGroup {
5050
private final boolean logLimitsOnJoin;
5151
private final boolean asyncGolums;
5252
private final boolean showLimitMessages;
53+
private final boolean stackedPlantsCountAsOne;
5354
private static final List<EntityType> DISALLOWED = Arrays.asList(
5455
EntityType.TNT,
5556
EntityType.EVOKER_FANGS,
@@ -96,6 +97,8 @@ public Settings(Limits addon) {
9697
asyncGolums = addon.getConfig().getBoolean("async-golums", true);
9798
// Show or suppress the "hit the limit" player notifications
9899
showLimitMessages = addon.getConfig().getBoolean("show-limit-messages", true);
100+
// Count a stackable plant column (sugar cane, bamboo) as a single plant
101+
stackedPlantsCountAsOne = addon.getConfig().getBoolean("stacked-plants-count-as-one", false);
99102

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

272+
/**
273+
* @return true if a column of stackable plants (sugar cane, bamboo) counts as one
274+
* plant; false if every segment counts (default)
275+
*/
276+
public boolean isStackedPlantsCountAsOne() {
277+
return stackedPlantsCountAsOne;
278+
}
279+
269280
public Map<GeneralGroup, Integer> getGeneral() {
270281
return general;
271282
}

src/main/java/world/bentobox/limits/calculators/RecountCalculator.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,20 @@ private void scanAsync(Environment env, Chunk chunk) {
161161
}
162162

163163
private void scanColumn(Environment env, ChunkSnapshot chunkSnapshot, int x, int z, int minY, int maxY) {
164+
boolean stackedAsOne = addon.getSettings().isStackedPlantsCountAsOne();
165+
NamespacedKey below = null;
164166
for (int y = minY; y < maxY; y++) {
165167
BlockData blockData = chunkSnapshot.getBlockData(x, y, z);
166168
if (Tag.SLABS.isTagged(blockData.getMaterial())
167169
&& ((Slab) blockData).getType().equals(Slab.Type.DOUBLE)) {
168170
checkBlock(env, blockData);
169171
}
170-
checkBlock(env, blockData);
172+
NamespacedKey key = bll.fixMaterial(blockData);
173+
// Stacked-plants-as-one: segments sitting on the same plant are not counted
174+
if (!(stackedAsOne && BlockLimitsListener.STACKABLE.contains(key) && key.equals(below))) {
175+
checkBlock(env, blockData);
176+
}
177+
below = key;
171178
}
172179
}
173180

src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ public class BlockLimitsListener implements Listener {
6565
private static final List<NamespacedKey> DO_NOT_COUNT = List.of(Material.LAVA.getKey(), Material.WATER.getKey(),
6666
Material.AIR.getKey(), Material.FIRE.getKey(), Material.END_PORTAL.getKey(),
6767
Material.NETHER_PORTAL.getKey());
68-
private static final List<NamespacedKey> STACKABLE = List.of(Material.SUGAR_CANE.getKey(), Material.BAMBOO.getKey());
68+
/** Plants that grow as a vertical column on top of themselves. */
69+
public static final List<NamespacedKey> STACKABLE = List.of(Material.SUGAR_CANE.getKey(), Material.BAMBOO.getKey());
6970

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

413+
/** True if the given material normalises to the same stackable plant key. */
414+
private static boolean isSamePlant(Material below, NamespacedKey plantKey) {
415+
return VARIANT_MAP.getOrDefault(below, below).getKey().equals(plantKey);
416+
}
417+
410418
/**
411419
* Map variant materials to their canonical form.
412420
*/
@@ -432,6 +440,12 @@ private int process(Block b, BlockData blockData, boolean add) {
432440
if (DO_NOT_COUNT.contains(fixMaterial(blockData)) || !addon.inGameModeWorld(b.getWorld())) {
433441
return -1;
434442
}
443+
// Stacked-plants-as-one: a segment sitting on the same plant is not counted,
444+
// limited, or decremented — only the base segment represents the plant.
445+
if (addon.getSettings().isStackedPlantsCountAsOne() && STACKABLE.contains(fixMaterial(blockData))
446+
&& isSamePlant(b.getRelative(BlockFace.DOWN).getType(), fixMaterial(blockData))) {
447+
return -1;
448+
}
435449
Environment env = envOf(b.getWorld());
436450
return addon.getIslands().getIslandAt(b.getLocation()).map(i -> {
437451
String id = i.getUniqueId();

src/main/resources/config.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ show-limit-messages: true
4646
blocklimits:
4747
HOPPER: 10
4848

49+
# Count stackable plants (SUGAR_CANE, BAMBOO) as a single plant no matter how tall
50+
# they grow. When false (default), every segment of the plant counts toward the limit.
51+
# Run a recount (/<gamemode> limits recount) after changing this so stored counts
52+
# match the new counting rule.
53+
stacked-plants-count-as-one: false
54+
4955
# Override block limits in the nether for this game mode.
5056
# Uncomment and add entries to set nether-only limits.
5157
#blocklimits-nether:

src/test/java/world/bentobox/limits/SettingsTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,18 @@ void testShowLimitMessagesDisabled() {
115115
assertFalse(s.isShowLimitMessages());
116116
}
117117

118+
@Test
119+
void testStackedPlantsCountAsOneDefaultsFalse() {
120+
assertFalse(settings.isStackedPlantsCountAsOne());
121+
}
122+
123+
@Test
124+
void testStackedPlantsCountAsOneEnabled() {
125+
config.set("stacked-plants-count-as-one", true);
126+
Settings s = new Settings(addon);
127+
assertTrue(s.isStackedPlantsCountAsOne());
128+
}
129+
118130
@Test
119131
void testGetGeneralEmpty() {
120132
// Default config.yml does not have ANIMALS or MOBS entries in entitylimits

src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ void setUp() throws Exception {
138138
doAnswer(invocation -> null).when(addon).log(anyString());
139139
doAnswer(invocation -> null).when(addon).logError(anyString());
140140
when(addon.isCoveredGameMode(anyString())).thenReturn(true);
141+
// Limits settings: stacked-plants defaults false (mock default), messages on
141142
when(addon.getSettings()).thenReturn(limitsSettings);
142143
when(limitsSettings.isShowLimitMessages()).thenReturn(true);
143144
when(addon.inGameModeWorld(any(World.class))).thenReturn(false);
@@ -966,6 +967,61 @@ void testEntityChangeBlockFarmlandRemovesCropAbove() {
966967
assertEquals(1, ibc.getBlockCount(Material.DIRT.getKey()));
967968
}
968969

970+
// --- Stacked plants count as one (#53) ---
971+
972+
@Test
973+
void testStackedPlantSegmentNotCountedWhenEnabled() {
974+
when(limitsSettings.isStackedPlantsCountAsOne()).thenReturn(true);
975+
Block below = mockBlock(Material.SUGAR_CANE, new Location(world, 100, 64, 100));
976+
Block block = mockBlock(Material.SUGAR_CANE, blockLocation);
977+
when(block.getRelative(BlockFace.DOWN)).thenReturn(below);
978+
979+
BlockState replacedState = mock(BlockState.class);
980+
BlockPlaceEvent event = new BlockPlaceEvent(block, replacedState, block,
981+
new ItemStack(Material.SUGAR_CANE), player, true, EquipmentSlot.HAND);
982+
listener.onBlock(event);
983+
984+
assertFalse(event.isCancelled());
985+
IslandBlockCount ibc = listener.getIsland("test-island-id");
986+
assertTrue(ibc == null || ibc.getBlockCount(Material.SUGAR_CANE.getKey()) == 0);
987+
}
988+
989+
@Test
990+
void testStackedPlantBaseCountedWhenEnabled() {
991+
when(limitsSettings.isStackedPlantsCountAsOne()).thenReturn(true);
992+
Block block = mockBlock(Material.SUGAR_CANE, blockLocation);
993+
// Below is AIR from the mockBlock helper — this is the base segment
994+
995+
BlockState replacedState = mock(BlockState.class);
996+
BlockPlaceEvent event = new BlockPlaceEvent(block, replacedState, block,
997+
new ItemStack(Material.SUGAR_CANE), player, true, EquipmentSlot.HAND);
998+
listener.onBlock(event);
999+
1000+
assertEquals(1, listener.getIsland("test-island-id").getBlockCount(Material.SUGAR_CANE.getKey()));
1001+
}
1002+
1003+
@Test
1004+
void testStackedPlantBreakBaseDecrementsOnlyOneWhenEnabled() {
1005+
when(limitsSettings.isStackedPlantsCountAsOne()).thenReturn(true);
1006+
// Only the base was ever counted
1007+
IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock");
1008+
ibc.add(Environment.NORMAL, Material.SUGAR_CANE.getKey());
1009+
listener.setIsland("test-island-id", ibc);
1010+
1011+
when(world.getMaxHeight()).thenReturn(320);
1012+
Block bottomBlock = mockBlock(Material.SUGAR_CANE, new Location(world, 100, 65, 100));
1013+
when(bottomBlock.getY()).thenReturn(65);
1014+
Block midBlock = mockBlock(Material.SUGAR_CANE, new Location(world, 100, 66, 100));
1015+
when(midBlock.getY()).thenReturn(66);
1016+
when(bottomBlock.getRelative(BlockFace.UP)).thenReturn(midBlock);
1017+
1018+
BlockBreakEvent event = new BlockBreakEvent(bottomBlock, player);
1019+
listener.onBlock(event);
1020+
1021+
// Exactly the one counted base is removed; the stem walk must not run
1022+
assertEquals(0, listener.getIsland("test-island-id").getBlockCount(Material.SUGAR_CANE.getKey()));
1023+
}
1024+
9691025
// --- Block cascade tests ---
9701026

9711027
@Test

0 commit comments

Comments
 (0)